Chapter 02 · DbContext Lifecycle, Configuration, Dependency Injection, Factories, and Pooling
Configure Providers with DbContextOptions, OnConfiguring, and External Configuration
Compose EF Core provider and runtime behavior explicitly with DbContextOptions, OnConfiguring, external configuration, safe diagnostics, and observable provider evidence.
Learning outcomes
ServiceHub needs one context type to run in developer laptops, tests, and later server deployments without burying environment decisions inside entity classes. EF Core centralizes runtime behavior in DbContextOptions. The engineering challenge is not memorizing configuration methods; it is knowing which layer owns provider choice, connection material, diagnostics, and provider-specific options.
Construct and inject DbContextOptions
Explain when OnConfiguring runs and how IsConfigured prevents a fallback from fighting externally supplied provider configuration.
Configure SQLite provider options, command timeout, logging, detailed errors, and environment-sensitive diagnostics.
Prove which provider/connection/options the context actually received.
Diagnose conflicting provider registration and hard-coded secret/environment mistakes.
1. DbContextOptions is the immutable configuration recipe
DbContextOptions<ServiceHubContext> carries configuration into a context instance. The generic form is preferred when multiple context types exist because DI can resolve the options intended for the correct subtype. A clean context constructor accepts those options and passes them to the base class.
public sealed class ServiceHubContext : DbContext{ public ServiceHubContext(DbContextOptions<ServiceHubContext> options) : base(options) { } public DbSet<WorkOrder> WorkOrders => Set<WorkOrder>();}The entity classes do not need to know whether the database is SQLite, SQL Server, or PostgreSQL. Provider choice belongs at the application composition boundary. Provider-specific model decisions still exist, but hiding connection/environment logic in entities makes testing and deployment brittle.
2. Build options explicitly when there is no DI container
var options = new DbContextOptionsBuilder<ServiceHubContext>() .UseSqlite("Data Source=servicehub-lab.db", sqlite => sqlite.CommandTimeout(30)) .EnableDetailedErrors() .Options;await using var db = new ServiceHubContext(options);Console.WriteLine(db.Database.ProviderName);UseSqlite comes from the SQLite provider package. The nested provider-options callback configures provider/relational behavior such as command timeout. EnableDetailedErrors can help development diagnosis but has overhead. EnableSensitiveDataLogging can expose entity keys, parameter values, personally identifiable information, or secrets and should remain off by default.
| Option | Scope | Production judgment |
|---|---|---|
| UseSqlite / UseSqlServer / UseNpgsql | Provider selection and provider-specific configuration | Exactly one provider per context instance. |
| CommandTimeout | Database command execution timeout | Tune from workload/SLO evidence, not a universal number. |
| LogTo / UseLoggerFactory | EF diagnostic output | Prefer structured ILogger integration in hosted apps. |
| EnableDetailedErrors | Richer query materialization errors | Useful in safe diagnostics; measure overhead. |
| EnableSensitiveDataLogging | Include application values in diagnostics | Development-only by deliberate opt-in. |
3. OnConfiguring always runs
Microsoft documents that OnConfiguring is called regardless of whether options came from new, AddDbContext, or a factory. That makes it useful for non-conflicting additions such as a lab-only diagnostic switch. It also means an unconditional provider call can accidentally collide with externally composed configuration.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){ if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlite("Data Source=servicehub-lab.db"); }}For a production application, prefer explicit external configuration over a hidden fallback. The fallback is acceptable for a disposable teaching tool when it is obvious and documented. If the context is configured through DI with SQL Server later, IsConfigured prevents the SQLite fallback from adding a second provider.
4. External configuration keeps environment policy outside persistence types
{ "ConnectionStrings": { "ServiceHub": "Data Source=servicehub-lab.db" }, "EfDiagnostics": { "DetailedErrors": true, "SensitiveData": false }}var connectionString = configuration.GetConnectionString("ServiceHub") ?? throw new InvalidOperationException("ConnectionStrings:ServiceHub is missing.");services.AddDbContext<ServiceHubContext>((sp, options) =>{ var cfg = sp.GetRequiredService<IConfiguration>(); options.UseSqlite(connectionString, sqlite => sqlite.CommandTimeout(30)); if (cfg.GetValue<bool>("EfDiagnostics:DetailedErrors")) options.EnableDetailedErrors();});A SQLite file path is usually not a credential, but server connection strings often are. Store secrets using user-secrets for local development or an environment/managed secret store appropriate to the deployment. Never paste a real password into lesson code or source control.
5. Deliberately broken: configure two providers in one context
services.AddDbContext<ServiceHubContext>(o => o.UseSqlite("Data Source=servicehub.db"));services.ConfigureDbContext<ServiceHubContext>(o => o.UseSqlServer(configuration.GetConnectionString("ServiceHubSqlServer")));A single context instance must use one provider. Microsoft explicitly warns that configuring a different provider does not remove a previously configured provider; the context can fail when created. Configuration composition is appropriate for non-conflicting options such as logging/interceptors. If an environment needs a different provider, make provider selection explicit and register the context once for that environment.
Select one provider from validated deployment configuration, register it once, and verify Database.ProviderName plus the package/database version matrix during startup/tests. Do not silently “try SQLite then SQL Server.”
6. Observe the effective configuration
await using var db = serviceProvider.GetRequiredService<ServiceHubContext>();Console.WriteLine($"Provider: {db.Database.ProviderName}");Console.WriteLine($"Connection type: {db.Database.GetDbConnection().GetType().FullName}");Console.WriteLine($"Data source: {db.Database.GetDbConnection().DataSource}");Console.WriteLine(db.WorkOrders.Where(w => w.Priority >= 4).ToQueryString());Do not log the raw connection string in production because it may contain credentials. Provider name plus safe endpoint/database identifiers, package versions, and generated SQL shape usually provide enough evidence for diagnostics.
7. Hands-on lab: switch configuration sources without changing the context model
Create two disposable SQLite files—servicehub-dev.db and servicehub-test.db—selected by configuration. Keep the same ServiceHubContext and model.
# DevelopmentDOTNET_ENVIRONMENT=Development dotnet run --project src/ServiceHub.EfLab# Test configuration should point at a different disposable SQLite file.DOTNET_ENVIRONMENT=Test dotnet run --project src/ServiceHub.EfLab# Verify package and tool versionsdotnet list src/ServiceHub.EfLab packagedotnet tool run dotnet-ef -- --versionVerification checklist
- The context constructor only accepts
DbContextOptions<ServiceHubContext>. - Exactly one provider is configured per context.
- Development and Test choose different disposable files via external configuration.
- Sensitive-data logging remains false by default.
- The app prints safe provider/connection evidence without a password.
- Generated SQL still reflects SQLite syntax.
Check your understanding
- Why prefer DbContextOptions
over the non-generic options type in most derived contexts? - Is OnConfiguring skipped when AddDbContext is used?
- Can one context instance use two providers simultaneously?
- Why is a connection string in appsettings not automatically safe?
- What does ToQueryString prove?
Review the answers
The generic type prevents ambiguity and ensures DI resolves options for the intended context subtype.
No. OnConfiguring is always called and can add configuration.
No. Each context instance must use exactly one database provider.
Server connection strings commonly carry credentials or sensitive endpoints; configuration files can be committed or logged.
It shows a diagnostic SQL representation of the query; it is not an execution plan, runtime timing, or portability guarantee.
8. Production judgment and bridge
Make provider choice, connection delivery, logging, timeouts, and environment behavior explicit at the composition root. Record the effective provider/package/database versions and test the exact production-like provider where semantics matter. Configuration code should be boring, observable, and secure.
With options composed correctly, the next lesson lets ASP.NET Core own context creation through dependency injection. The critical question becomes scope: why AddDbContext defaults to a scoped lifetime and why a singleton must not capture that scoped context.
Authoritative references
- DbContext Lifetime, Configuration, and Initialization — DbContextOptions, OnConfiguring, provider selection, DI, and configuration composition
- Database Providers — provider package and compatibility guidance
- Connection Strings — connection-string configuration and secret warnings
- Simple logging — LogTo, detailed errors, and sensitive-data logging
- Safe storage of app secrets in development — Secret Manager and development secret guidance