DbContext Lifecycle & Pooling
Overview
Understanding DbContext lifecycle is critical for building performant and reliable EF Core applications. DbContext manages connections, tracks changes, and coordinates database operations.
DbContext Lifecycle
Scoped Lifetime (Recommended)
// ASP.NET Core - Scoped per HTTP request
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
}
// Controller usage - injected per request
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context; // New instance per request
}
public async Task<IActionResult> Get()
{
var products = await _context.Products.ToListAsync();
return Ok(products);
} // Context disposed automatically at end of request
}
Why Scoped?
- Thread Safety: DbContext is NOT thread-safe
- Change Tracking: Tracks entities for a single unit of work
- Connection Management: Manages database connection lifecycle
- Memory: Prevents memory leaks from long-lived contexts
Common Mistakes
// ❌ WRONG: Singleton DbContext (NOT thread-safe!)
services.AddSingleton<AppDbContext>();
// ❌ WRONG: Long-lived DbContext
public class MyService
{
private readonly AppDbContext _context;
public MyService()
{
_context = new AppDbContext(); // Never disposed!
}
}
// ✅ CORRECT: Scoped or manually disposed
public class MyService
{
public async Task ProcessData()
{
await using var context = new AppDbContext();
// Use context
} // Disposed automatically
}
DbContext Pooling
Without Pooling
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// Each request:
// 1. Creates new DbContext
// 2. Initializes internal services
// 3. Allocates memory
// 4. Disposes everything
// = Overhead on every request
With Pooling (20-30% Faster)
services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString),
poolSize: 128); // Default is 1024
// Each request:
// 1. Reuses DbContext from pool
// 2. Resets state
// 3. Returns to pool when done
// = Much faster!
Pooling Configuration
// Custom pool size
services.AddDbContextPool<AppDbContext>(
options => options.UseSqlServer(connectionString),
poolSize: 256); // Adjust based on concurrent requests
// Pool size guidelines:
// - Low traffic: 32-64
// - Medium traffic: 128 (default for AddDbContextPool)
// - High traffic: 256-512
// - Very high traffic: 1024 (AddDbContext default)
Pooling Constraints
When using pooling, you CANNOT:
// ❌ Store instance state in DbContext
public class AppDbContext : DbContext
{
public int TenantId { get; set; } // BAD: Won't reset between requests!
}
// ✅ Use options pattern instead
public class AppDbContext : DbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AppDbContext(
DbContextOptions<AppDbContext> options,
IHttpContextAccessor httpContextAccessor)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
// Get tenant ID on demand
private int TenantId =>
int.Parse(_httpContextAccessor.HttpContext.User.FindFirst("TenantId").Value);
}
Creating DbContext Instances
Method 1: Dependency Injection (Recommended)
public class ProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<Product> GetProductAsync(int id)
{
return await _context.Products.FindAsync(id);
}
}
Method 2: DbContextFactory (For long-lived services)
// Registration
services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// Usage in singleton service
public class BackgroundService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public BackgroundService(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task ProcessBatch()
{
await using var context = await _contextFactory.CreateDbContextAsync();
// Use context for this batch
} // Context disposed
}
Method 3: Manual Creation (For console apps)
public class Program
{
static async Task Main(string[] args)
{
await using var context = new AppDbContext();
var products = await context.Products.ToListAsync();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
} // Context disposed
}
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer("connection string");
}
}
Performance Comparison
// Benchmark results (1000 requests)
// Without pooling: 150ms average per request
// With pooling: 100ms average per request
// Improvement: 33% faster
[MemoryDiagnoser]
public class DbContextBenchmark
{
[Benchmark(Baseline = true)]
public async Task WithoutPooling()
{
await using var context = new AppDbContext();
var product = await context.Products.FindAsync(1);
}
[Benchmark]
public async Task WithPooling()
{
await using var context = _factory.CreateDbContext();
var product = await context.Products.FindAsync(1);
}
}
Best Practices
✅ DO
- Use scoped lifetime in web applications
- Enable DbContext pooling for better performance
- Dispose DbContext after use (use
usingorawait using) - Create new DbContext for each unit of work
- Use
IDbContextFactoryfor long-lived services
❌ DON'T
- Make DbContext singleton
- Store instance state in DbContext when using pooling
- Share DbContext across threads
- Keep DbContext alive longer than necessary
- Pass DbContext between layers (use repository/service pattern)
Interview Questions
Q: Why should DbContext be scoped, not singleton?
A: DbContext is not thread-safe and maintains change tracking state for a single unit of work. Singleton would cause:
- Thread safety issues
- Change tracking conflicts
- Memory leaks from tracked entities
- Connection pooling problems
Q: How does DbContext pooling improve performance?
A: Pooling reuses DbContext instances instead of creating new ones for each request. This eliminates:
- Object allocation overhead
- Internal services initialization
- Configuration parsing Results in 20-30% performance improvement under load.
Q: When would you use IDbContextFactory instead of DI?
A: Use IDbContextFactory when:
- Working with singleton or long-lived services
- Background services that process batches
- Need multiple DbContext instances in parallel
- Console applications or non-web scenarios
Q: What happens if you don't dispose DbContext?
A: Without disposal:
- Database connections not returned to pool
- Memory leaks from tracked entities
- ChangeTracker holding references
- Potential connection exhaustion
Practice Exercise
Create a web API that:
- Uses DbContext with pooling
- Implements a background service with IDbContextFactory
- Demonstrates proper disposal patterns
- Benchmarks performance with/without pooling