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.

Intermediate80–100 minutesfactory + design-time tooling labEF Core 10.0.11 · .NET 10SQLite provider 10.0.11 baselineLast reviewed: August 2026

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.

01

Use AddDbContextFactory and IDbContextFactory to create explicit runtime units of work.

02

Dispose factory-created contexts correctly.

03

Use separate contexts for parallel independent work.

04

Distinguish runtime IDbContextFactory from IDesignTimeDbContextFactory used by dotnet-ef tooling.

05

Make design-time creation deterministic without leaking production secrets.

1. Runtime factory means “create a fresh unit when I ask”

csharp · register a runtime factory
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.”

csharp · one operation, one factory-created context
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.

csharp · Blazor-style event handler pattern
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

csharp · safe parallel fan-out
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.

csharp · explicit design-time creation for dotnet-ef
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”

csharp · not 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).

Repair

Use the framework-provided IDbContextFactory<TContext> or PooledDbContextFactory<TContext>. Make ownership explicit with using/await using.

6. Observe runtime versus design-time creation

csharp · runtime ContextId evidence
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
text · design-time tool evidence
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 info and migrations list.
  • Remove any accidental production endpoint/secret from source.

Check your understanding

  1. Who disposes a context created by IDbContextFactory?
  2. Why can a factory be preferable in a long-lived UI scope?
  3. Are IDbContextFactory and IDesignTimeDbContextFactory the same abstraction?
  4. Does a design-time factory need production credentials?
  5. 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

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