Analyzer rule
LC030: Review DbContext Lifetime Mismatches
LC030 flags singletons and hosted services that store a DbContext, a lifetime mismatch that leads to threading errors and stale data. Use a factory.
- Default severity
- Info
- Code fix
- Manual only
- Category
- Architecture
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC030.severity = error
LC030: Review DbContext Lifetime Mismatches
In Plain Terms
Imagine you have one paintbrush (DbContext) that everyone in the class has to share at the same time. Paint gets mixed up, bristles break, and everyone makes a mess! Instead, give each person their own paintbrush from a box (IDbContextFactory) when they need one, and put it back when they’re done.
Goal
Flag proven long-lived types that store or directly receive a DbContext, and flag explicit singleton
DbContext registrations, so the lifetime can be reviewed.
The Problem
DbContext is NOT thread-safe and is designed to be short-lived (Scoped). Storing it on a long-lived service is risky and can lead to:
- Threading Crashes: Two concurrent requests try to use the context at the same time, causing an
InvalidOperationException. - Memory Leaks: The Change Tracker keeps every entity ever fetched in memory forever.
- Data Corruption: Stale data from previous requests persists in the context.
Example Violation
// Review needed: hosted services are long-lived by default
public sealed class Worker : BackgroundService
{
private readonly AppDbContext _db;
public Worker(AppDbContext db) => _db = db;
}
The Fix
Review the service lifetime first. If the type is long-lived, inject IDbContextFactory<T> instead or move the DbContext
usage to a scoped component. LC030 intentionally has no code fix because the right correction depends on the service
ownership model and registration.
// Correct
public class MyService
{
private readonly IDbContextFactory<AppDbContext> _factory;
public MyService(IDbContextFactory<AppDbContext> factory) => _factory = factory;
public void DoWork()
{
using var db = _factory.CreateDbContext();
// ...
}
}
Analyzer Logic
ID: LC030
Category: Architecture
Severity: Info
Algorithm
- Targets:
- Fields and stored properties whose type derives from
Microsoft.EntityFrameworkCore.DbContext, including static members on proven long-lived types. Computed getter properties are ignored only when the getter directly creates a fresh context withIDbContextFactory<TContext>.CreateDbContext()ornew TContext(). - Constructor parameters whose type derives from
DbContext, when the type has no storedDbContextmember to report. - DI calls that register a
DbContextitself as singleton.
- Fields and stored properties whose type derives from
- Strict long-lived proof:
- Report when the containing type implements
IHostedService, inheritsBackgroundService, has a conventional ASP.NET Core middlewareInvoke/InvokeAsync(HttpContext, ...)shape, is registered withAddHostedService<T>(), or is registered withAddSingleton(...). - For singleton factory registrations, report the concrete implementation type only when the factory directly returns a constructed implementation, such as
AddSingleton<IWorker>(_ => new Worker(...)). - Report direct
AddSingleton<TDbContext>()andAddDbContext<TContext>(..., ServiceLifetime.Singleton)registrations. - DI registration matching is limited to
Microsoft.Extensions.DependencyInjectionmethods whose first parameter isIServiceCollection, avoiding project helpers that happen to share names such asAddSingleton. - Stay quiet for controllers, page models, view components,
IMiddleware,AddScoped,AddTransient, factories, options, and generic service classes with no long-lived proof.
- Report when the containing type implements
- Optional project configuration:
dotnet_code_quality.LC030.detection_mode = expandedenables conservative name-based review hints such as*Singleton*,*HostedService, and*BackgroundWorker.dotnet_code_quality.LC030.long_lived_types = MyApp.IAlwaysSingleton, MyApp.LongLivedBasetreats matching base types or interfaces as long-lived. Separate the full type names with commas: in.editorconfigand.globalconfigfiles;and#start a comment, so anything after them is ignored.
Notes
LC030 is intentionally manual-only. It does not provide a code fix because the correct change may be IDbContextFactory<TContext>, creating a scope through IServiceScopeFactory, changing registration lifetimes, or moving work into a scoped collaborator.
Test Cases
Violations
public sealed class Worker : BackgroundService { private readonly AppDbContext _db; ... }
public sealed class Worker : BackgroundService { private static AppDbContext _db; ... }
public sealed class Worker : BackgroundService { public AppDbContext Db { get; } = new AppDbContext(); ... }
public sealed class Worker : BackgroundService { private AppDbContext Db => _services.GetRequiredService<AppDbContext>(); ... }
public sealed class AuditMiddleware { private readonly AppDbContext _db; public Task InvokeAsync(HttpContext ctx) => Task.CompletedTask; }
services.AddSingleton<IWorker>(_ => new Worker(db)); // factory returns a singleton implementation that stores DbContext
services.AddSingleton<AppDbContext>(); // DbContext registered as singleton
services.AddDbContext<AppDbContext>(contextLifetime: ServiceLifetime.Singleton);
Valid
public class MyController { public MyController(AppDbContext db) { ... } } // Controllers are scoped
public sealed class ScopedAuditMiddleware : IMiddleware { public ScopedAuditMiddleware(AppDbContext db) { ... } } // IMiddleware can be scoped
services.AddScoped<MyService>();
private readonly IDbContextFactory<AppDbContext> _factory;
private AppDbContext Db => _factory.CreateDbContext(); // Fresh context per access
public sealed class PlainType { private static AppDbContext _db; } // No long-lived-type proof