'NET 5 and EF: how to use AddPooledDbContextFactory in liu of DbContext in services

I recently came across AddPooledDbContextFactory concept as part of my NET 5 self-education piece and am keen to implement it properly. However, I am not sure how to use it with generics that I generally use.

Example of my current setup:

public void ConfigureServices(IServiceCollection services)
   {
        services.AddDbContext<TestDbContext>(
                (s, o) => o.UseNpgsql(Configuration.GetConnectionString("DatabaseConnection"))
                           .UseLoggerFactory(s.GetRequiredService<ILoggerFactory>()));

// other code //
    }

my repository generic:

    public class Repository<T> : IRepository<T> where T
    {
        private readonly TestDbContext _dbContext;
        public Repository(TestDbContext dbContext)
        {
            _dbContext = dbContext;
        }
        

        public async Task Create(T entity)
        {
           await _dbContext.Set<T>().AddAsync(entity);
           await _dbContext.SaveChangesAsync();
        }

        // other methods //
   }

this is invoked in following manner as example:

public class WeatherForecastController : ControllerBase
{
    
    private readonly IRepository<Test> testRepo;

    public WeatherForecastController(IRepository<Test> testRepo)
    {
        this.testRepo= testRepo;
    }

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> GetAsync()
    {
        await testRepo.Create(new Test { Name = "Superman" });
        
        // other code
    }

}

I would like to convert this to use the new AddPooledDbContextFactory concept but cannot find enough documentation to figure out how to do this.

Atm only thing that comes to mind is using statements at each method but that doesn't make sense.

Any advice on this?



Solution 1:[1]

Documentation is not yet complete and is in progress, you track this issue https://github.com/dotnet/EntityFramework.Docs/issues/2523

You can also a look at the tests for AddPooledDbContextFactory to see how to register DbContext with https://github.com/dotnet/efcore/search?q=AddPooledDbContextFactory

for example to register DbContext:

services.AddPooledDbContextFactory<TContext>(ob =>
    ob.UseSqlServer("ConnectionString").EnableServiceProviderCaching(false), poolSize: 32)

Then in your class, inject an IDbContextFactory<TContext> and use it like this:

using(var context = _factory.CreateDbContext())
{
    var orders = await context.Orders.Where(o => o.Id > 3).ToListAsync();
}

According to this post:

Note that the DbContext instances created in this way are not managed by the application's service provider and therefore must be disposed by the application

You can also check out this post to see how to use IDbContextFactory: https://docs.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-5.0

Solution 2:[2]

@Aeseir your code looks good to me. You are following best practices and you don't need to change it.

You are using the Repository Pattern, so your Repository class has all of your query logic which helps create loosely coupled and maintainable code.

In your ConfigureServices, calling: services.AddDbContext<TestDbContext>() registers TestDbContext with Scoped service lifetime. This is the way that DbContext is designed to work, and it will also work well with ASP.NET controllers, since they have a Scoped lifetime as well.

You did not show your code for registering IRepository, but that service lifetime should be Scoped as well. Btw, you can tell BuildServiceProvider() to validate scope registrations:

builder.Services.BuildServiceProvider(validateScopes: true);

Since DbContext is designed to have a Scoped service lifetime, and since your IRepository and Controller services are Scoped as well, every request gets brand new:

  1. Controller
  2. IRepository
  3. DbContext

Those services are used for the request and then Diposed. This is how ASP.NET is intended to work.

Apparently at some point, DbContext pooling has been introduced to improve performance. In this case, EF Core manages a pool of context instances for you and resets them after each request. This can improve performance, although in some situations, the benefit might be small. See MSDN documentation for more details.

I think for use with ASP.NET controllers (i.e. the code you posted above) all you need to do to take advantage of EF Core context pooling is call AddDbContextPool():

builder.Services.AddDbContextPool<ApplicationDbContext>(/* ... */);

However, if you needed to use DbContext in services registered with Singleton lifetime, then the pattern above would not work well. Because when a Scoped service gets used in a Singleton service, the Scoped service is essentially a Singleton. Each request would not get a new DbContext, nor a reset one from the pool. (See QUESTION below.)

In that case, you might want to use the DbContext factory pattern instead:

builder.Services.AddDbContextFactory<ApplicationDbContext>(/* ... */);

Or, if you want to use context pooling with a factory pattern:

builder.Services.AddPooledDbContextFactory<ApplicationDbContext>(/* ... */);

The DbContextFactory can then be used in other services through constructor injection. For example:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

The injected factory can then be used to construct DbContext instances in the controller code. For example:

public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

Keep in mind that when you call CreateDbContext(), context instances are not managed by the service provider and therefore must be disposed by the application. Hence you need to Dispose of them yourself, such as in the example above which does so with the using statement.

QUESTION

I am doing my best to understand this stuff and explain it, but I might be wrong, so please call out an inaccuracies in my post.

When using AddDbContextPool(), does the DbContext get registered as a Singleton or Scoped?

I found in MSDN documentation that it's effectively registered as a Singleton:

Context pooling works by reusing the same context instance across requests; this means that it's effectively registered as a Singleton, and the same instance is reused across multiple requests (or DI scopes). This means that special care must be taken when the context involves any state that may change between requests.

However, I have found that if AddDbContextPool() is used along with true for validatedScopes:

builder.Services.BuildServiceProvider(validateScopes: true)

When DbContext is consumed from another service which is registered as a Singleton, the following exception is thrown:

System.InvalidOperationException: 'Cannot consume scoped service 'ApplicationDbContext' from singleton 'IRepository'.'

Hence why I stated above that DbContext still gets Scoped service lifetime.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2