Why is DbContext registered as Scoped?
Quick Answer
DbContext is registered as Scoped because it is a lightweight, stateful, not thread-safe unit-of-work. One instance per request lets every repository and service in that request share the same change tracker and transaction, and the context is disposed when the request ends. Making it a singleton would share one change tracker across all concurrent requests — a correctness and threading disaster.
Because DbContext is stateful and not thread-safe. It's a unit of work — it tracks every entity you touch during a request so it can save them all in one transaction. Scoped means one context per HTTP request, shared by all the repositories in that request, and disposed at the end. If it were a singleton, every concurrent request would share one change tracker and one connection across multiple threads, which throws concurrency exceptions and mixes up whose data is whose. If it were transient, each repository would get its own context and they'd no longer be part of the same unit of work. When I need a context in a singleton or a background service, I use IDbContextFactory or create my own scope.
AddDbContext<T>() registers the context as Scoped by default. The reasons:
- It is not thread-safe. A single
DbContextcannot be used by two threads at once. Requests run concurrently, so a shared instance would be used by many threads — leading to exceptions like “A second operation was started on this context before a previous operation completed.” - It is a unit of work. The context tracks every entity you load or add. Scoped-per-request means all changes made while handling one request are tracked together and saved in one
SaveChanges()/ one transaction. - It accumulates state. The change tracker grows as you query. A long-lived context would keep tracked entities forever, leaking memory and returning stale data.
- It is cheap to create. Constructing a context per request is inexpensive; the connection itself is pooled underneath.
If you need a context outside a request (a background job, a singleton service), you inject IDbContextFactory<T> or create a scope with IServiceScopeFactory and resolve the context inside it.
// WRONG: capturing a scoped DbContext in a singleton -> captive dependency
public class CacheWarmer // registered as Singleton
{
private readonly AppDbContext _db; // this instance lives forever
public CacheWarmer(AppDbContext db) => _db = db;
}
// RIGHT: create a scope when you need the context
public class CacheWarmer
{
private readonly IServiceScopeFactory _scopes;
public CacheWarmer(IServiceScopeFactory scopes) => _scopes = scopes;
public async Task WarmAsync()
{
using var scope = _scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// use db here, disposed with the scope
}
}
- Every repository in one request shares one change tracker and one transaction
- Avoids the 'second operation started on this context' threading error
- The context and its tracked entities are released at the end of each request
- Cheap to create per request; the underlying DB connection is pooled anyway
Practise the whole thread the way an interviewer walks it — each question builds on the last.