Proper management of the DbContext lifecycle is crucial for application performance and stability.
While in many cases, registering DbContext with a scoped lifetime is simple enough, there are scenarios where more control is needed. EF Core offers more flexibility on DbContext creation.
In this blog post, I will show how to use DbContext, DbContextFactory and their pooled versions. These features allow for greater flexibility and efficiency, especially in applications that require high performance or have specific threading models.
Using DbContext
DbContext
is the heart of EF Core, it establishes connection with a database and allows performing CRUD operations.
The DbContext
class is responsible for:
- Managing database connections: opens and closes connections to the database as needed.
- Change tracking: keeps track of changes made to entities so they can be persisted to the database.
- Query execution: translates LINQ queries to SQL and executes them against the database.
When working with DbContext, you should be aware of the following nuances:
- Not thread-safe: should not be shared across multiple threads simultaneously.
- Lightweight: designed to be instantiated and disposed frequently.
- Stateful: tracks entity states for change tracking and identity resolution.
Let's explore how you can register DbContext in the DI container:
csharpbuilder.Services.AddDbContext<ApplicationDbContext>(options => { options.EnableSensitiveDataLogging().UseNpgsql(connectionString); }); public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
DbContext
is registered as Scoped in DI container.
It has the lifetime of current scope which equals to the current request duration:
csharpapp.MapPost("/api/authors", async ( [FromBody] CreateAuthorRequest request, ApplicationDbContext context, CancellationToken cancellationToken) => { var author = request.MapToEntity(); context.Authors.Add(author); await context.SaveChangesAsync(cancellationToken); var response = author.MapToResponse(); return Results.Created($"/api/authors/{author.Id}", response); });
You can also manually create a scope and resolve the DbContext:
csharpusing (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); await dbContext.Database.MigrateAsync(); }
DbContext
being a Scoped dependency is pretty flexible, you can inject the same instance of DbContext
into the Controller/Minimal API endpoint, service, repository.
But sometimes, you need more control on when DbContext
is created and disposed.
Such control you can get with DbContextFactory
.
Using DbContextFactory
In some use cases, such as background services, multi-threaded applications, or factories that create services, you might need full control to create and dispose DbContext
instance.
The IDbContextFactory<TContext>
is a service provided by EF Core that allows creating of DbContext instances on demand.
It ensures that each instance is configured correctly and can be used safely without being tied directly to the DI container's service lifetime.
You can register IDbContextFactory<TContext>
in the following way, similar to DbContext
:
csharpbuilder.Services.AddDbContextFactory<ApplicationDbContext>(options => { options.EnableSensitiveDataLogging().UseNpgsql(connectionString); });
IDbContextFactory
is registered as Singleton in the DI container.
You can the CreateDbContext
or CreateDbContextAsync
from the IDbContextFactory
to create a DbContext:
csharppublic class HostedService : IHostedService { private readonly IDbContextFactory<ApplicationDbContext> _contextFactory; public HostedService(IDbContextFactory<ApplicationDbContext> contextFactory) { _contextFactory = contextFactory; } public async Task StartAsync(CancellationToken cancellationToken) { await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); var books = await context.Books.ToListAsync(cancellationToken: cancellationToken); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }
DbContext
is disposed when the using block ends.
You should use IDbContextFactory<TContext>
with caution - make sure that you won't create too many database connections.
You can register both DbContext
and IDbContextFactory
at the same time.
You need to do a small tweak for this and set the optionsLifetime
to Singleton
as the IDbContextFactory
is registered as Singleton:
csharpbuilder.Services.AddDbContext<ApplicationDbContext>(options => { options.EnableSensitiveDataLogging().UseNpgsql(connectionString); }, optionsLifetime: ServiceLifetime.Singleton); builder.Services.AddDbContextFactory<ApplicationDbContext>(options => { options.EnableSensitiveDataLogging().UseNpgsql(connectionString); });
Using DbContext Pooling
DbContext pooling is a feature introduced in EF Core that allows for reusing DbContext instances, reducing the overhead of creating and disposing of contexts frequently.
DbContext pooling maintains a pool of pre-configured DbContext instances. When you request a DbContext, it provides one from the pool. When you're done, it resets the state and returns the instance to the pool for reuse.
This feature is crucial for high performance scenarios or when you need to create a lot of database connections.
Registration of Pooled DbContext is straightforward:
csharpbuilder.Services.AddDbContextPool<ApplicationDbContext>(options => { options.EnableSensitiveDataLogging().UseNpgsql(connectionString); });
In your classes you inject a regular DbContext
without knowing that it is being pooled.
Using DbContextFactory Pooling
You can combine DbContextFactory
and DbContext pooling to create a pooled DbContextFactory.
This allows you to create DbContext instances on demand, which are also pooled for performance.
csharpbuilder.Services.AddPooledDbContextFactory<ApplicationDbContext>(options => { options.EnableSensitiveDataLogging().UseNpgsql(connectionString); });
The API for using pooled factory is the same:
csharpawait using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); var books = await context.Books.ToListAsync(cancellationToken: cancellationToken);
Each CreateDbContextAsync
call retrieves a DbContext from the pool.
After disposing, the DbContext is returned to the pool.
In your classes you inject a regular DbContextFactory
without knowing that it is being pooled.
Summary
Managing the DbContext lifetime is essential for building efficient EF Core applications. By leveraging DbContextFactory and DbContext pooling, you can gain greater control over context creation and optimize performance.
Key Takeaways:
- DbContextFactory: use when you need to create DbContext instances on demand, especially in multi-threaded or background tasks.
- DbContext Pooling: use to reduce the overhead of creating and disposing of DbContext instances frequently.
- Pooled DbContextFactory: combine both features to create pooled DbContext instances on demand.
- Always Dispose DbContext Instances: use using statements or ensure that contexts are disposed or returned to the pool.
- Avoid Thread Safety Issues: do not share DbContext instances across threads.
Hope you find this blog post useful. Happy coding!