Skip to content

Latest commit

 

History

History
670 lines (499 loc) · 30.1 KB

entity-framework-core-integration-overview.md

File metadata and controls

670 lines (499 loc) · 30.1 KB
titledescriptionms.dateuidzone_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

Entity Framework Core overview

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:

Overview of EF Core

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

How .NET Aspire can help

.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.

Use .NET Aspire to create an EF Core context

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...}

Use EF Core to add and enrich 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:

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...}

Use EF Core interceptors with .NET Aspire

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.

Use EF Core with dynamic connection strings in .NET Aspire

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.

Use EF Core context factories in .NET Aspire

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.

Use EF Core context pooling in .NET Aspire

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.

See also

close