Pooled DbContext factory

In general, DbContext is a lightweight object: creating and disposing it doesn't involve a database operation, and most applications can do this without noticeable performance impact. However, each context instance sets up various internal services and objects necessary to perform its tasks, and the overhead of continuously doing so may be significant in high-performance scenarios. For these cases, EF Core can pool your context instances:

  • When you dispose your context, EF Core resets its state and stores it in an internal pool.

  • The next time a new instance is requested, the pooled instance is returned instead of setting up a new one.

Context pooling allows you to pay context setup costs only once at program start-up, rather than continuously. Which is why Smartstore configures a pooled DbContext factory on start-up. When a context instance is requested from the factory it does one of the following:

  • Looks up a free / idle instance in pool and return it, or

  • Creates an instance, return it and put back into the pool on return / dispose.

By default, the maximum number of instances retained by the pool (pool size) is set to 1024 and can be altered via appsettings.json using the DbContextPoolSize setting. Once the pool size is exceeded, new context instances aren’t cached and EF reverts to non-pooling behavior, creating instances as needed.

Smartstore registers a scoped service factory for the SmartDbContext service type, which internally resolves an instance from the pool. This instance is then returned to the pool when the request completes. So in general you don’t need to call the CreateDbContext method of IDbContextFactory<SmartDbContext>.

However, there may be situations where working with IDbContextFactory is beneficial:

  • If your code does not run within the scope of an HTTP request like the SmartDbContextSink that resolves a SmartDbContext instance periodically, triggered by a timer.

public async Task EmitBatchAsync(IEnumerable<LogEvent> batch)
{
    var db = CreateDbContext();

    if (db != null)
    {
        await using (db)
        {
            db.MinHookImportance = HookImportance.Important;
            db.Logs.AddRange(batch.Select(CovertLogEvent));
            await db.SaveChangesAsync();
        }
    }
}

public Task OnEmptyBatchAsync()
    => Task.CompletedTask;

private static SmartDbContext CreateDbContext()
{
    var engine = EngineContext.Current;
    var factory = engine.Application.Services
        .Resolve<IDbContextFactory<SmartDbContext>>();
        
    return factory.CreateDbContext();
}
  • If you need to access SmartDbContext inside a singleton class like the SettingFactory.

private readonly IDbContextFactory<SmartDbContext> _dbContextFactory;

// ...

private IDisposable GetOrCreateDbContext(out SmartDbContext db)
{
    db = _scope?.ResolveOptional<SmartDbContext>() ??
         _httpContextAccessor.HttpContext?.RequestServices?.GetService<SmartDbContext>();

    if (db != null)
    {
        // Don't dispose request scoped main db instance.
        return ActionDisposable.Empty;
    }

    // Fetch a fresh DbContext if no scope is given.
    db = _dbContextFactory.CreateDbContext();

    return db;
}
  • In very long-running processes that load or write a lot of entities. This can gradually decrease performance, because the change tracker tracks too many entities. In this case it may be beneficial to resolve a fresh instance from pool, instead of detaching all entities, after a batch completes.

    • EXAMPLE

Last updated

Was this helpful?