September 16 2025
This issue is made possible thanks to ZZZ Projects, who help keep this newsletter free for everyone. A huge shout-out to them for their support of our community.
Thousands of developers fixed EF Core performance - with just one method.
👉 Discover this extension
DbContext is the heart of EF Core - but it’s easy to misuse. The big rules:
• A DbContext represents a unit of work and should be short-lived. • It is not thread-safe; never share one instance across concurrent operations. • In ASP.NET Core, the default and usually correct choice is a scoped DbContext per request. For work outside the request scope (background services, singletons, UI apps), use a factory to create fresh contexts on demand.
This post shows how to wire it correctly, when to choose each registration, and the traps to avoid.
// Program.csbuilder.Services.AddDbContext<AppDbContext>(options =>options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
When to use: MVC/Minimal API controllers, Razor Pages, SignalR hubs - anything that lives inside a web request.
Why: You get one context per request automatically; EF Core handles connection usage efficiently.
builder.Services.AddDbContextFactory<AppDbContext>(options =>options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
How to use it:
public sealed class ReportService(IDbContextFactory<AppDbContext> factory){ public async Task<IReadOnlyList<OrderDto>> GetAsync(CancellationToken ct) { await using var db = await factory.CreateDbContextAsync(ct); return await db.Orders .Where(o => o.Status == OrderStatus.Completed) .Select(o => new OrderDto(o.Id, o.Total)) .ToListAsync(ct); }}
When to use:
• Background/hosted services (IHostedService, BackgroundService) • Any singleton service that needs a DbContext • Desktop/Blazor apps where you want a fresh context per operation
Why: Factories create clean, short-lived contexts without relying on ambient scopes.
builder.Services.AddDbContextPool<AppDbContext>(options =>options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
When to use: High-throughput APIs when the context configuration is stable and stateless.
Why: Reuses DbContext instances from a pool for lower allocation overhead. Caveat: don’t stash per-request state in your context; pooled instances are reset and reused.
app.MapGet("/orders/{id:int}", async (int id, AppDbContext db, CancellationToken ct) =>{ var order = await db.Orders.FindAsync([id], ct); return order is null ? Results.NotFound() : Results.Ok(order);});
Why it works: DI gives you a scoped context bound to the request; one unit-of-work, no leaks.
public sealed class CleanupService(IServiceScopeFactory scopes, ILogger<CleanupService> log) : BackgroundService{ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var scope = scopes.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); // One short-lived unit of work var cutoff = DateTime.UtcNow.AddDays(-30); db.RemoveRange(db.AuditLogs.Where(x => x.CreatedAt < cutoff)); await db.SaveChangesAsync(stoppingToken); await Task.Delay(TimeSpan.FromHours(6), stoppingToken); } }}
Alternative (my go-to):
public sealed class CleanupService(IDbContextFactory<AppDbContext> factory, ILogger<CleanupService> log) : BackgroundService{ protected override async Task ExecuteAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { await using var db = await factory.CreateDbContextAsync(ct); // same cleanup logic … await Task.Delay(TimeSpan.FromHours(6), ct); } }}
Why: Hosted services don’t have a request scope; you must create one (or use a factory).
public sealed class PricingCache(IDbContextFactory<AppDbContext> factory, IMemoryCache cache){ public async Task<decimal> GetPriceAsync(int productId, CancellationToken ct) { if (cache.TryGetValue(productId, out decimal price)) return price; await using var db = await factory.CreateDbContextAsync(ct); price = await db.Products .Where(p => p.Id == productId) .Select(p => p.Price) .FirstAsync(ct); cache.Set(productId, price, TimeSpan.FromMinutes(10)); return price; }}
Why: Singletons must not capture scoped DbContext; creating a fresh one per call avoids threading and lifetime bugs.
builder.Services.AddDbContextPool<AppDbContext>(o =>{ o.UseNpgsql(builder.Configuration.GetConnectionString("Default")); // Keep options stateless; avoid per-request mutable state});
Tip: Pooling helps allocation/throughput. It doesn’t make DbContext thread-safe. Still one context per request.
• Sharing one DbContext across threads
Symptom: “A second operation started on this context before a previous operation was completed.” Fix: One context per unit of work; never run parallel queries on the same context. Use separate contexts. • Injecting DbContext into singletons
Fix: Inject IDbContextFactory
• Keeping contexts alive too long
Fix: Keep them short-lived; long-lived contexts bloat change trackers and retain connections.
• Using pooling with per-request state
Fix: Don’t put user-specific state on the context (e.g., CurrentUserId field). Pooled contexts are reused.
• Trying to “speed up” by running multiple queries concurrently on one context
Fix: Either serialize the work or create multiple contexts. DbContext is not thread-safe.
• Pooling reduces allocations under heavy load; measure with your workload. • Thread-safety checks: EF Core can detect some multi-thread misuse; you can disable checks to squeeze perf, but only if you’re absolutely sure no concurrency occurs on the same context. I rarely recommend turning them off.
For more EF Core best practices, see EF Core Interceptors and 4 EF Core Performance Tips.
Managing DbContext lifetime correctly is the difference between a stable, high-performance EF Core app and one that randomly throws concurrency errors or quietly leaks memory. The rule of thumb is simple:
• Default: AddDbContext (scoped) - one context per request. • Background/singleton: AddDbContextFactory - create short-lived contexts on demand. • High throughput: AddDbContextPool - recycle contexts, but only if your configuration is stateless.
Think of DbContext as a notepad for a single unit of work: you grab a fresh sheet, write your changes, and toss it away when you’re done. Don’t pass it around the whole office, and don’t try to write on it with two pens at once.
By keeping contexts short-lived, isolated, and created in the right way for your workload, you’ll avoid the classic EF Core pitfalls - while keeping your app fast, predictable, and easy to maintain.
That's all from me for today.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.