Chapter 02 · DbContext Lifecycle, Configuration, Dependency Injection, Factories, and Pooling
IDbContextFactory, Design-Time Factories, Background Workers, and Blazor-Style Lifetimes
Use runtime and design-time DbContext factories for mismatched lifetimes, parallel independent work, tooling, workers, tests, and Blazor-style operations.
Learning outcomes
Not every application scope maps cleanly to one database unit of work. A Blazor-style UI circuit can live much longer than a single operation; a worker can process many messages; a request can intentionally run multiple independent units; tests may need fresh contexts on demand. EF Core’s runtime factory and design-time factory solve these creation problems at different phases.
Use AddDbContextFactory and IDbContextFactory to create explicit runtime units of work.
Dispose factory-created contexts correctly.
Use separate contexts for parallel independent work.
Distinguish runtime IDbContextFactory from IDesignTimeDbContextFactory used by dotnet-ef tooling.
Make design-time creation deterministic without leaking production secrets.
1. Runtime factory means “create a fresh unit when I ask”
builder.Services.AddDbContextFactory<ServiceHubContext>(options => options.UseSqlite(builder.Configuration.GetConnectionString("ServiceHub") ?? "Data Source=servicehub-factory.db"));
Inject
IDbContextFactory<ServiceHubContext> into a
service whose own lifetime is not the desired context lifetime.
Each CreateDbContext/CreateDbContextAsync
call returns a context you own and must dispose. The factory
separates “service lifetime” from “database unit-of-work
lifetime.”
public sealed class WorkOrderReader(IDbContextFactory<ServiceHubContext> factory){ public async Task<WorkOrderDto?> GetAsync(int id, CancellationToken ct) { await using var db = await factory.CreateDbContextAsync(ct); return await db.WorkOrders .Where(w => w.Id == id) .Select(w => new WorkOrderDto(w.Id, w.CustomerName, w.Priority)) .SingleOrDefaultAsync(ct); }}
2. Multiple units inside one request or UI lifetime
A single request may have two deliberately separate read units, or a long-lived UI component may perform many user actions over minutes. Holding one context for the entire lifetime accumulates tracking state and risks concurrency if events overlap. Create a context per operation instead.
private async Task RefreshAsync(){ await using var db = await _factory.CreateDbContextAsync(); _rows = await db.WorkOrders .AsNoTracking() .OrderByDescending(w => w.Priority) .Take(50) .ToListAsync();}
Factory use does not imply no-tracking; that choice is query-specific. The key point is that each user action receives a fresh identity map and change tracker unless the application explicitly needs a longer unit.
3. Parallel independent operations: factory per branch
var highPriorityTask = Task.Run(async () =>{ await using var db = await factory.CreateDbContextAsync(ct); return await db.WorkOrders.CountAsync(w => w.Priority >= 4, ct);}, ct);var openTask = Task.Run(async () =>{ await using var db = await factory.CreateDbContextAsync(ct); return await db.WorkOrders.CountAsync(w => w.Status == WorkOrderStatus.Open, ct);}, ct);var results = await Task.WhenAll(highPriorityTask, openTask);
The Task.Run wrapper is usually unnecessary for
naturally asynchronous database I/O in server code; it is shown
only to make branch ownership visible. Prefer calling two async
methods directly, each of which creates its own context.
Parallel database work also increases connection/database load,
so measure whether concurrency helps.
4. Design-time factory is for tools, not runtime work
IDesignTimeDbContextFactory<ServiceHubContext>
belongs to the
Microsoft.EntityFrameworkCore.Design tooling path.
When EF tools find an implementation in the context/startup
project, they can use it to create the context for
migrations/scaffolding without booting the full application
host. That is different from IDbContextFactory,
which application code uses at runtime.
public sealed class ServiceHubDesignTimeFactory : IDesignTimeDbContextFactory<ServiceHubContext>{ public ServiceHubContext CreateDbContext(string[] args) { var options = new DbContextOptionsBuilder<ServiceHubContext>() .UseSqlite("Data Source=servicehub-design.db") .Options; return new ServiceHubContext(options); }}
Use a disposable development/design-time database or load
configuration securely. Never hard-code a production password
just to make dotnet ef work. Migration generation
primarily needs a correctly configured model/provider;
deployment credentials should remain a separate concern.
5. Deliberately broken: one static context hidden behind a “factory”
public sealed class BadContextFactory{ private static ServiceHubContext? _context; public ServiceHubContext Get() => _context ??= CreateContext();}
This returns the same mutable context repeatedly. It defeats the lifetime boundary, creates unsupported concurrency, and allows tracking state to survive across callers. A real factory creates a new context (or leases a properly reset pooled context in the next lesson).
Use the framework-provided
IDbContextFactory<TContext> or
PooledDbContextFactory<TContext>. Make
ownership explicit with using/await using.
6. Observe runtime versus design-time creation
await using var first = await factory.CreateDbContextAsync();await using var second = await factory.CreateDbContextAsync();Console.WriteLine(first.ContextId);Console.WriteLine(second.ContextId);Console.WriteLine(first.ContextId == second.ContextId); // expected false
dotnet tool run dotnet-ef -- --versiondotnet ef dbcontext info --project src/ServiceHub.EfLabdotnet ef migrations list --project src/ServiceHub.EfLab
dbcontext info and migration commands prove the
tools can instantiate the context; they do not prove production
deployment credentials, runtime DI scope, or transaction
behavior.
7. Hands-on lab: worker + factory + tooling
-
Refactor the Chapter 02 worker to inject
IDbContextFactory<ServiceHubContext>. - Create and dispose one context per work item.
- Run two independent count operations concurrently with different contexts and print both IDs.
- Add a design-time factory pointing at a disposable design database.
-
Run
dotnet ef dbcontext infoandmigrations list. - Remove any accidental production endpoint/secret from source.
Check your understanding
- Who disposes a context created by IDbContextFactory?
- Why can a factory be preferable in a long-lived UI scope?
- Are IDbContextFactory and IDesignTimeDbContextFactory the same abstraction?
- Does a design-time factory need production credentials?
- What should parallel EF branches share: one context or one model/provider configuration with separate contexts?
Review the answers
The application code that created/leased it.
It lets each UI operation get a fresh unit of work instead of retaining one tracker for the entire circuit/session.
No. The former is a runtime creation abstraction; the latter is specifically for EF design-time tools.
No. Use a safe development/design-time configuration sufficient for model/tool operations.
Separate contexts can share configuration/model caches, but not the mutable context instance.
8. Production judgment and bridge
Use a runtime factory when the DI scope does not map to a unit of work or when multiple independent units are required inside one scope. Treat factory-created contexts as owned resources, pass cancellation, and keep transaction boundaries explicit. Use a design-time factory when tooling needs a deterministic creation path—not as a backdoor around application configuration.
Factories solve creation. The final lesson asks whether context creation itself is expensive enough to optimize with pooling, and why pooling turns state hygiene into a correctness and tenant-isolation concern.
Authoritative references
- DbContext Lifetime, Configuration, and Initialization — AddDbContextFactory and factory-created context ownership
- Design-time DbContext Creation — tool creation paths and IDesignTimeDbContextFactory
- EF Core .NET CLI tools — dbcontext and migration tool commands
- ASP.NET Core Blazor with EF Core — factory-oriented context lifetime for Blazor-style UI