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.

Intermediate80–100 minutesDI scopes + web/worker labEF Core 10.0.11 · .NET 10SQLite provider 10.0.11 baselineLast reviewed: August 2026

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.

01

Register ServiceHubContext with AddDbContext and explain the default scoped lifetime.

02

Use constructor/minimal-API injection without manually newing or disposing the scoped context.

03

Prove request scopes receive independent ContextId values.

04

Diagnose singleton-to-scoped capture errors and background-service lifetime mismatches.

05

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.

csharp · register ServiceHubContext in a web app
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

csharp · minimal API endpoint with scoped DbContext
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

csharp · log ContextId and request trace identifier
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

csharp · invalid lifetime dependency
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.

Repair

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.

csharp · scope per background iteration
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

text · web/worker acceptance run
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

  • AddDbContext is 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

  1. What lifetime does AddDbContext use by default?
  2. Why is that usually safe for ASP.NET Core requests?
  3. Can a singleton safely store an injected scoped DbContext?
  4. What does IServiceScopeFactory solve in a BackgroundService?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n