Chapter 02 · DbContext Lifecycle, Configuration, Dependency Injection, Factories, and Pooling
AddDbContext, Scoped Lifetimes, Dependency Injection, and Request-Oriented Applications
Map DbContext ownership to ASP.NET Core dependency-injection scopes, prove request isolation, and repair singleton/background-service lifetime mismatches.
Learning outcomes
ServiceHub is moving from a console lab to a small ASP.NET Core
API. The built-in dependency-injection (DI) container can create
ServiceHubContext, but DI only helps if service
lifetime matches the unit of work. EF Core’s
AddDbContext defaults to scoped,
which normally maps one context instance to one HTTP request.
Register ServiceHubContext with AddDbContext and explain the default scoped lifetime.
Use constructor/minimal-API injection without manually newing or disposing the scoped context.
Prove request scopes receive independent ContextId values.
Diagnose singleton-to-scoped capture errors and background-service lifetime mismatches.
Create explicit scopes for hosted/background work when a factory is not used.
1. DI lifetime is an ownership contract
ASP.NET Core creates a service scope per HTTP request. Services
registered as scoped are shared within that
scope and disposed when the scope ends.
AddDbContext<TContext> registers the context
as scoped by default, so one request receives one context
instance when the context is injected multiple times through the
same scope.
var builder = WebApplication.CreateBuilder(args);var cs = builder.Configuration.GetConnectionString("ServiceHub") ?? "Data Source=servicehub-web.db";builder.Services.AddDbContext<ServiceHubContext>(options => options.UseSqlite(cs));var app = builder.Build();
This default works well when one request maps to one unit of work. It is not a statement that every request should hold a context for minutes, nor that long-lived UI circuits or background services should share the same scoped instance indefinitely.
2. Request-oriented injection keeps ownership simple
app.MapGet("/work-orders/open", async ( ServiceHubContext db, CancellationToken cancellationToken) =>{ var rows = await db.WorkOrders .Where(w => w.Status == WorkOrderStatus.Open) .OrderByDescending(w => w.Priority) .Select(w => new { w.Id, w.CustomerName, w.Priority }) .ToListAsync(cancellationToken); return Results.Ok(rows);});
The endpoint does not call Dispose; the request
scope owns the injected context. The query is still
provider-translated SQL. Dependency injection changes object
construction and lifetime, not relational semantics.
3. Observe scopes instead of assuming them
app.MapGet("/diag/context", (HttpContext http, ServiceHubContext db) =>{ return Results.Ok(new { Request = http.TraceIdentifier, Context = db.ContextId.ToString(), Tracked = db.ChangeTracker.Entries().Count() });});
Two independent requests should normally report different context IDs. Multiple consumers resolved inside one request scope should see the same scoped instance unless a factory creates additional contexts. Correlate the context ID with the request trace ID and EF command logs when diagnosing unexpected tracking or transaction boundaries.
4. Deliberately broken: singleton captures scoped DbContext
builder.Services.AddSingleton<DispatchDashboard>();public sealed class DispatchDashboard{ private readonly ServiceHubContext _db; public DispatchDashboard(ServiceHubContext db) => _db = db;}
A singleton lives for the application lifetime while the context belongs to a request scope. With scope validation, the built-in container rejects this mismatch with an error such as “Cannot consume scoped service ... from singleton ...”. Without proper validation, the design can still leak a context beyond its intended scope and create concurrent/stale tracking behavior.
Do not inject a scoped DbContext into a singleton. For request
work, make the consumer scoped. For singleton/background
components, create a scope per unit of work or inject
IDbContextFactory<ServiceHubContext>.
5. BackgroundService is singleton-like: create a unit scope
Hosted services registered with
AddHostedService are created by the root container
and do not automatically get a fresh request scope for each
iteration. Use IServiceScopeFactory to create a
scope per message/batch when you want scoped services.
public sealed class DispatchWorker(IServiceScopeFactory scopeFactory) : BackgroundService{ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await using var scope = scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService<ServiceHubContext>(); var next = await db.WorkOrders .Where(w => w.Status == WorkOrderStatus.Open) .OrderByDescending(w => w.Priority) .FirstOrDefaultAsync(stoppingToken); // Process one explicit unit of work here. await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } }}
Do not resolve one context once in the worker constructor and reuse it for the lifetime of the process. A fresh scope bounds tracker state and allows scoped dependencies to be disposed predictably.
6. Scope does not equal transaction
A request scope is a DI lifetime. It does not automatically wrap
every operation in one database transaction. EF Core commonly
uses a transaction around a single SaveChanges when
the provider supports transactions, while multiple queries and
saves require explicit transactional design when atomicity
matters. Later chapters cover transactions and execution
strategies.
| Concept | Owned by | What it guarantees |
|---|---|---|
| DI scope | Application service provider | Object reuse/disposal boundary |
| DbContext unit of work | Application/EF usage | Tracking/identity and SaveChanges coordination |
| ADO.NET connection | Provider/driver/EF | Command transport; often opened late and closed early |
| Database transaction | Database/provider | Atomicity/isolation according to provider/database rules |
7. Hands-on lab: prove request and worker isolation
dotnet run --project src/ServiceHub.Web# Call /diag/context twice and record Request + Context IDs.# Call /work-orders/open and inspect EF command logs.# Enable the intentionally invalid singleton consumer and observe scope validation.# Replace it with IServiceScopeFactory or IDbContextFactory.# Run two worker iterations and prove the ContextId changes per scope.
Verification checklist
-
AddDbContextis registered once with SQLite 10.0.11. - Independent requests have independent context IDs.
- The request scope owns disposal of injected contexts.
- The singleton/scoped mismatch is reproduced or validated conceptually.
- The worker creates a new scope per unit of work.
- No context is shared across parallel request/worker execution.
Check your understanding
- What lifetime does AddDbContext use by default?
- Why is that usually safe for ASP.NET Core requests?
- Can a singleton safely store an injected scoped DbContext?
- What does IServiceScopeFactory solve in a BackgroundService?
- Does one request scope imply one database transaction?
Review the answers
Scoped.
ASP.NET Core creates a separate DI scope per request, so each request normally gets a separate context.
No. The singleton outlives the scope and can capture/dispose/use the context incorrectly.
It lets a long-lived worker create a fresh scope and scoped context per explicit unit of work.
No. DI lifetime and transaction lifetime are different mechanisms.
8. Production judgment and bridge
Use scoped AddDbContext when the request scope
naturally matches the unit of work. Log request correlation plus
context IDs when diagnosing leaks, and watch long-running
requests, large tracker counts, and disposed-context errors. For
workers, UI circuits, parallel branches, and multiple units
inside one scope, a factory is often a clearer boundary.
The next lesson introduces runtime
IDbContextFactory and design-time
IDesignTimeDbContextFactory, which solve different
creation problems and should not be conflated.
Authoritative references
- DbContext Lifetime, Configuration, and Initialization — AddDbContext default scoped lifetime and concurrent-access guidance
- Dependency injection in ASP.NET Core — service lifetimes, scopes, and validation
- Background tasks with hosted services — hosted-service patterns and scoped services
- Using scoped services within a BackgroundService — explicit scope creation for workers