Skip to main content

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

// 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?

  1. Thread Safety: DbContext is NOT thread-safe
  2. Change Tracking: Tracks entities for a single unit of work
  3. Connection Management: Manages database connection lifecycle
  4. 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

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 using or await using)
  • Create new DbContext for each unit of work
  • Use IDbContextFactory for 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:

  1. Uses DbContext with pooling
  2. Implements a background service with IDbContextFactory
  3. Demonstrates proper disposal patterns
  4. Benchmarks performance with/without pooling

Additional Resources