title | description | ms.date | uid | zone_pivot_groups |
---|---|---|---|---|
Entity Framework Core overview | Learn how to optimize the performance of .NET Aspire Entity Framework Core integrations using their context objects. | 03/03/2025 | database/use-entity-framework-db-contexts | entity-framework-client-integration |
In a cloud-native solution, such as those .NET Aspire is built to create, microservices often need to store data in relational databases. .NET Aspire includes integrations that you can use to ease that task, some of which use the Entity Framework Core (EF Core) object-relational mapper (O/RM) approach to streamline the process.
Developers use O/RMs to work with databases using code objects instead of SQL queries. EF Core automatically codes database interactions by generating SQL queries based on Language-Integrated Query (LINQ) queries. EF Core supports various database providers, including SQL Server, PostgreSQL, and MySQL, so it's easy to interact with relational databases while following object-oriented principles.
The most commonly used .NET Aspire EF Core client integrations are:
- Cosmos DB Entity Framework Core integration
- MySQL Pomelo Entity Framework Core integration
- Oracle Entity Framework Core integration
- PostgreSQL Entity Framework Core integration
- SQL Server Entity Framework Core integration
O/RMs create a model that matches the schema and relationships defined in the database. Code against this model to query the data, create new records, or make other changes. In EF Core the model consists of:
- A set of entity classes, each of which represents a table in the database and its columns.
- A context class that represents the whole database.
An entity class might look like this:
usingSystem.ComponentModel.DataAnnotations;namespaceSupportDeskProject.Data;publicsealedclassSupportTicket{publicintId{get;set;}[Required]publicstringTitle{get;set;}=string.Empty;[Required]publicstringDescription{get;set;}=string.Empty;}
This entity class represents a database table with three columns: Id, Title, and Description.
A context class must inherit from xref:System.Data.Entity.DbContext and looks like this:
usingMicrosoft.EntityFrameworkCore;usingSystem.Reflection.Metadata;namespaceSupportDeskProject.Data;publicclassTicketContext(DbContextOptionsoptions):DbContext(options){publicDbSet<SupportTicket>Tickets=>Set<SupportTicket>();}
This context represents a database with a single table of support tickets. An instance of the context is usually created for each unit of work in the database. For example, a unit of work might be the creation of a new customer, and require changes in the Customers and Addresses tables. Once the unit of work is complete, you should dispose of the context.
Note
For more information about creating models in EF Core, see Creating and Configuring a Model in the EF Core documentation.
Once you've created a model, you can use LINQ to query it:
using(vardb=newTicketContext()){vartickets=awaitdb.Tickets.Where(t =>t.Title="Unable to log on").OrderBy(t =>t.Description).ToListAsync();}
Note
EF Core also supports creating, modifying, and deleted records and complex queries. For more information, see Querying Data and Saving Data
.NET Aspire is designed to help build observable, production-ready, cloud-native solutions that consist of multiple microservices. It orchestrates multiple projects, each of which may be a microservice written by a dedicated team, and connects them to each other. It provides integrations that make it easy to connect to common services, such as databases.
If you want to use EF Core in any of your microservices, .NET Aspire can help by:
Managing the database container, or a connection to an existing database, centrally in the App Host project and passing its reference to any project that uses it.
[!IMPORTANT] In .NET Aspire, EF Core is implemented by client integrations, not hosting integrations. The centralized management of the database in the App Host doesn't involve EF Core, which runs in consuming microservice projects instead. For more information, see Cosmos DB Hosting integration, MySQL Pomelo Hosting integration, Oracle Hosting integration, PostgreSQL Hosting integration, or SQL Server Hosting integration.
Providing EF Core-aware integrations that make it easy to create contexts in microservice projects. There are EF Core integrations for SQL Server, MySQL, PostgreSQL, Oracle, Cosmos DB, and other popular database systems.
To use EF Core in your microservice, you must:
- Define the EF Core model with entity classes and context classes.
- Create an instance of the data context, using the reference passed from the App Host, and add it to the Dependency Injection (DI) container.
- When you want to interact with the database, obtain the context from DI and use it to execute LINQ queries against the database as normal for any EF Core code.
:::image type="content" source="media/ef-core-aspire-architecture-thumb.png" lightbox="media/ef-core-aspire-architecture-large.png" alt-text="A diagram showing how .NET Aspire utilizes EF Core to interact with a database.." :::
Both defining the EF Core model and querying the database are the same in .NET Aspire projects as in any other EF Core app. However, creating the data context differs. In the rest of this article, you'll learn how to create an configure EF Core contexts in .NET Aspire project.
In EF Core, a context is a class used to interact with the database. Contexts inherit from the xref:Microsoft.EntityFrameworkCore.DbContext class. They provide access to the database through properties of type DbSet<T>
, where each DbSet
represents a table or collection of entities in the database. The context also manages database connections, tracks changes to entities, and handles operations like saving data and executing queries.
The .NET Aspire EF Core client integrations each include extension methods named Add{DatabaseSystem}DbContext
, where {DatabaseSystem} is the name identifying the database product you're using. For example, consider the SQL Server EF Core client integration, the method is named xref:Microsoft.Extensions.Hosting.AspireSqlServerEFCoreSqlClientExtensions.AddSqlServerDbContext%2A and for the PostgreSQL client integration, the method is named xref:Microsoft.Extensions.Hosting.AspireEFPostgreSqlExtensions.AddNpgsqlDbContext%2A.
These .NET Aspire add context methods:
- Check that a context of the same type isn't already registered in the dependency injection (DI) container.
- Use the connection name you pass to the method to get the connection string from the application builder. This connection name must match the name used when adding the corresponding resource to the app host project.
- Apply the
DbContext
options, if you passed them. - Add the specified
DbContext
to the DI container with context pooling enabled. - Apply the recommended defaults, unless you've disabled them through the .NET Aspire EF Core settings:
- Enable tracing.
- Enable health checks.
- Enable connection resiliency.
Use these .NET Aspire add context methods when you want a simple way to create a context and don't yet need advanced EF Core customization.
:::zone pivot="sql-server-ef"
builder.AddSqlServerDbContext<ExampleDbContext>(connectionName:"database");
Tip
For more information about SQL Server hosting and client integrations, see .NET Aspire SQL Server Entity Framework Core integration.
:::zone-end :::zone pivot="postgresql-ef"
builder.AddNpgsqlDbContext<ExampleDbContext>(connectionName:"database");
Tip
For more information about PostgreSQL hosting and client integrations, see .NET Aspire PostgreSQL Entity Framework Core integration.
:::zone-end :::zone pivot="oracle-ef"
builder.AddOracleDatabaseDbContext<ExampleDbContext>(connectionName:"database");
Tip
For more information about Oracle Database hosting and client integrations, see .NET Aspire Oracle Entity Framework Core integration.
:::zone-end :::zone pivot="mysql-ef"
builder.AddMySqlDbContext<ExampleDbContext>(connectionName:"database");
Tip
For more information about MySQL hosting and client integrations, see .NET Aspire Pomelo MySQL Entity Framework Core integration.
:::zone-end
You obtain the ExampleDbContext
object from the DI container in the same way as for any other service:
publicclassExampleService(ExampleDbContextcontext){// Use context...}
Alternatively, you can add a context to the DI container using the standard EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextPool* method, as commonly used in non-.NET Aspire projects:
:::zone pivot="sql-server-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>{varconnectionString=builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.");options.UseSqlServer(connectionString);});
:::zone-end :::zone pivot="postgresql-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>{varconnectionString=builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.");options.UseNpgsql(connectionString);});
:::zone-end :::zone pivot="oracle-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>{varconnectionString=builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.");options.UseOracle(connectionString);});
:::zone-end :::zone pivot="mysql-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>{varconnectionString=builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.");options.UseMySql(connectionString);});
:::zone-end
You have more flexibility when you create the context in this way, for example:
- You can reuse existing configuration code for the context without rewriting it for .NET Aspire.
- You can choose not to use EF Core context pooling, which may be necessary in some circumstances. For more information, see Use EF Core context pooling in .NET Aspire
- You can use EF Core context factories or change the lifetime for the EF Core services. For more information, see Use EF Core context factories in .NET Aspire
- You can use dynamic connection strings. For more information, see Use EF Core with dynamic connection strings in .NET Aspire
- You can use EF Core interceptors that depend on DI services to modify database operations. For more information, see Use EF Core interceptors in .NET Aspire
By default, a context configured this way doesn't include .NET Aspire features, such as telemetry and health checks. To add those features, each .NET Aspire EF Core client integration includes a method named Enrich\<DatabaseSystem\>DbContext
. These enrich context methods:
- Apply an EF Core settings object, if you passed one.
- Configure connection retry settings.
- Apply the recommended defaults, unless you've disabled them through the .NET Aspire EF Core settings:
- Enable tracing.
- Enable health checks.
- Enable connection resiliency.
Note
You must add a context to the DI container before you call an enrich method.
:::zone pivot="sql-server-ef"
builder.EnrichSqlServerDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="postgresql-ef"
builder.EnrichNpgsqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="oracle-ef"
builder.EnrichOracleDatabaseDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="mysql-ef"
builder.EnrichMySqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end
Obtain the context from the DI container using the same code as the previous example:
publicclassExampleService(ExampleDbContextcontext){// Use context...}
EF Core interceptors allow developers to hook into and modify database operations at various points during the execution of database queries and commands. You can use them to log, modify, or suppress operations with your own code. Your interceptor must implement one or more interface from the xref:Microsoft.EntityFrameworkCore.Diagnostics.IInterceptor interface.
Interceptors that depend on DI services are not supported by the .NET Aspire Add\<DatabaseSystem\>DbContext
methods. Use the EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextPool* method and call the xref:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.AddInterceptors* method in the options builder:
:::zone pivot="sql-server-ef"
builder.Services.AddDbContextPool<ExampleDbContext>((serviceProvider,options)=>{options.UseSqlServer(builder.Configuration.GetConnectionString("database"));options.AddInterceptors(serviceProvider.GetRequiredService<ExampleInterceptor>());});builder.EnrichSqlServerDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="postgresql-ef"
builder.Services.AddDbContextPool<ExampleDbContext>((serviceProvider,options)=>{options.UseNpgsql(builder.Configuration.GetConnectionString("database"));options.AddInterceptors(serviceProvider.GetRequiredService<ExampleInterceptor>());});builder.EnrichNpgsqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="oracle-ef"
builder.Services.AddDbContextPool<ExampleDbContext>((serviceProvider,options)=>{options.UseOracle(builder.Configuration.GetConnectionString("database"));options.AddInterceptors(serviceProvider.GetRequiredService<ExampleInterceptor());});builder.EnrichOracleDatabaseDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="mysql-ef"
builder.Services.AddDbContextPool<ExampleDbContext>((serviceProvider,options)=>{options.UseMySql(builder.Configuration.GetConnectionString("database"));options.AddInterceptors(serviceProvider.GetRequiredService<ExampleInterceptor>());});builder.EnrichMySqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end
Note
For more information about EF Core interceptors and their use, see Interceptors.
Most microservices always connect to the same database with the same credentials and other settings, so they always use the same connection string unless there's a major change in infrastructure. However, you may need to change the connection string for each request. For example:
- You might offer your service to multiple tenants and need to use a different database depending on which customer made the request.
- You might need to authenticate the request with a different database user account depending on which customer made the request.
For these requirements, you can use code to formulate a dynamic connection string and then use it to reach the database and run queries. However, this technique isn't supported by the .NET Aspire Add\<DatabaseSystem\>DbContext
methods. Instead you must use the EF Core method to create the context and then enrich it:
:::zone pivot="sql-server-ef"
varconnectionStringWithPlaceHolder=builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.");varconnectionString=connectionStringWithPlaceHolder.Replace("{DatabaseName}","ContosoDatabase");builder.Services.AddDbContext<ExampleDbContext>(options =>options.UseSqlServer(connectionString??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichSqlServerDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="postgresql-ef"
varconnectionStringWithPlaceHolder=builder.Configuration.GetConnectionString("database")thrownewInvalidOperationException("Connection string 'database' not found.");varconnectionString=connectionStringWithPlaceHolder.Replace("{DatabaseName}","ContosoDatabase");builder.Services.AddDbContext<ExampleDbContext>(options =>options.UseNpgsql(connectionString??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichNpgsqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="oracle-ef"
varconnectionStringWithPlaceHolder=builder.Configuration.GetConnectionString("database")thrownewInvalidOperationException("Connection string 'database' not found.");varconnectionString=connectionStringWithPlaceHolder.Replace("{DatabaseName}","ContosoDatabase");builder.Services.AddDbContext<ExampleDbContext>(options =>options.UseOracle(connectionString??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichOracleDatabaseDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="mysql-ef"
varconnectionStringWithPlaceHolder=builder.Configuration.GetConnectionString("database")thrownewInvalidOperationException("Connection string 'database' not found.");varconnectionString=connectionStringWithPlaceHolder.Replace("{DatabaseName}","ContosoDatabase");builder.Services.AddDbContext<ExampleDbContext>(options =>options.UseMySql(connectionString??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichMySqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end
The above code replaces the place holder {DatabaseName}
in the connection string with the string ContosoDatabase
, at run time, before it creates the context and enriches it.
An EF Core context is an object designed to be used for a single unit of work. For example, if you want to add a new customer to the database, you might need to add a row in the Customers table and a row in the Addresses table. You should get the EF Core context, add the new customer and address entities to it, call xref:Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync*, and then dispose the context.
In many types of web application, such as ASP.NET applications, each HTTP request closely corresponds to a single unit of work against the database. If your .NET Aspire microservice is an ASP.NET application or a similar web application, you can use the standard EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextPool* method described above to register a context that is tied to the current HTTP request. Remember to call the .NET Aspire Enrich\<DatabaseSystem\>DbContext
method to gain health checks, tracing, and other features. When you use this approach, the context lifetime is tied to the web request. You don't have to call the xref:Microsoft.EntityFrameworkCore.DbContext.Dispose* method when the unit of work is complete.
Other application types, such as ASP.NET Core Blazor, don't necessarily align each request with a unit of work, because they use dependency injection with a different service scope. In such apps, you may need to perform multiple units of work, each with a different context, within a single HTTP request and response. To implement this approach, you can register a context factory, by calling the EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddPooledDbContextFactory* method. This method also partners well with the .NET Aspire Enrich\<DatabaseSystem\>DbContext
methods:
:::zone pivot="sql-server-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseSqlServer(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichSqlServerDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="postgresql-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseNpgsql(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichNpgsqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="oracle-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseOracle(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichOracleDatabaseDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end :::zone pivot="mysql-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseMySql(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));builder.EnrichMySqlDbContext<ExampleDbContext>(configureSettings: settings =>{settings.DisableRetry=false;settings.CommandTimeout=30;// seconds});
:::zone-end
Notice that the above code adds and enriches a context factory in the DI container. When you retreive this from the container, you must add a line of code to create a context from it:
publicclassExampleService(IDbContextFactory<ExampleDbContext>contextFactory){using(varcontext= contextFactory.CreateDbContext()){// Use context...}}
Contexts created from factories in this way aren't disposed of automatically because they aren't tied to an HTTP request lifetime. You must make sure your code disposes of them. In this example, the using
code block ensures the disposal.
In EF Core a context is relatively quick to create and dispose of so most applications can set them up as needed without impacting their performance. However, the overhead is not zero so, if your microservice intensively creates contexts, you may observe suboptimal performance. In such situations, consider using a context pool.
Context pooling is a feature of EF Core. Contexts are created as normal but, when you dispose of one, it isn't destroyed but reset and stored in a pool. The next time your code creates a context, the stored one is returned to avoid the extra overhead of creating a new one.
In a .NET Aspire consuming project, there are three ways to use context pooling:
Use the .NET Aspire
Add\<DatabaseSystem\>DbContext
methods to create the context. These methods create a context pool automatically.Call the EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextPool* method instead of the EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContext* method.
:::zone pivot="sql-server-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>options.UseSqlServer(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end :::zone pivot="postgresql-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>options.UseNpgsql(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end :::zone pivot="oracle-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>options.UseOracle(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end :::zone pivot="mysql-ef"
builder.Services.AddDbContextPool<ExampleDbContext>(options =>options.UseMySql(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end
Call the EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddPooledDbContextFactory* method instead of the EF Core xref:Microsoft.Extensions.DependencyInjection.EntityFrameworkServiceCollectionExtensions.AddDbContextFactory* method.
:::zone pivot="sql-server-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseSqlServer(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end :::zone pivot="postgresql-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseNpgsql(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end :::zone pivot="oracle-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseOracle(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end :::zone pivot="mysql-ef"
builder.Services.AddPooledDbContextFactory<ExampleDbContext>(options =>options.UseMySql(builder.Configuration.GetConnectionString("database")??thrownewInvalidOperationException("Connection string 'database' not found.")));
:::zone-end
Remember to enrich the context after using the last two methods, as described above.
Important
Only the base context state is reset when it's returned to the pool. If you've manually changed the state of the DbConnection
or another service, you must also manually reset it. Additionally, context pooling prevents you from using OnConfiguring
to configure the context. See DbContext pooling for more information.