DEV Community

Cover image for How To Manage EF Core DbContext Lifetime
Anton Martyniuk
Anton Martyniuk

Posted on • Originally published at antondevtips.com on

How To Manage EF Core DbContext Lifetime

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.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

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:

builder.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

DbContext is registered as Scoped in DI container.
It has the lifetime of current scope which equals to the current request duration:

app.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);
});
Enter fullscreen mode Exit fullscreen mode

You can also manually create a scope and resolve the DbContext:

using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    await dbContext.Database.MigrateAsync();
}
Enter fullscreen mode Exit fullscreen mode

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:

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});
Enter fullscreen mode Exit fullscreen mode

IDbContextFactory is registered as Singleton in the DI container.

You can the CreateDbContext or CreateDbContextAsync from the IDbContextFactory to create a DbContext:

public 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;
}
Enter fullscreen mode Exit fullscreen mode

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:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
}, optionsLifetime: ServiceLifetime.Singleton);

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});
Enter fullscreen mode Exit fullscreen mode

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:

builder.Services.AddDbContextPool<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});
Enter fullscreen mode Exit fullscreen mode

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.

builder.Services.AddPooledDbContextFactory<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});
Enter fullscreen mode Exit fullscreen mode

The API for using pooled factory is the same:

await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
var books = await context.Books.ToListAsync(cancellationToken: cancellationToken);
Enter fullscreen mode Exit fullscreen mode

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.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

Top comments (0)