Analyzer rule
Spec: LC030 - Review DbContext Lifetime Mismatches
EF Core LINQ performance analyzer and Roslyn analyzer for catching query issues at compile time.
Spec: LC030 - Review DbContext Lifetime Mismatches
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.
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