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:

  1. Threading Crashes: Two concurrent requests try to use the context at the same time, causing an InvalidOperationException.
  2. Memory Leaks: The Change Tracker keeps every entity ever fetched in memory forever.
  3. 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

  1. 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 with IDbContextFactory<TContext>.CreateDbContext() or new TContext().
    • Constructor parameters whose type derives from DbContext, when the type has no stored DbContext member to report.
    • DI calls that register a DbContext itself as singleton.
  2. Strict long-lived proof:
    • Report when the containing type implements IHostedService, inherits BackgroundService, has a conventional ASP.NET Core middleware Invoke/InvokeAsync(HttpContext, ...) shape, is registered with AddHostedService<T>(), or is registered with AddSingleton(...).
    • 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>() and AddDbContext<TContext>(..., ServiceLifetime.Singleton) registrations.
    • DI registration matching is limited to Microsoft.Extensions.DependencyInjection methods whose first parameter is IServiceCollection, avoiding project helpers that happen to share names such as AddSingleton.
    • Stay quiet for controllers, page models, view components, IMiddleware, AddScoped, AddTransient, factories, options, and generic service classes with no long-lived proof.
  3. Optional project configuration:
    • dotnet_code_quality.LC030.detection_mode = expanded enables conservative name-based review hints such as *Singleton*, *HostedService, and *BackgroundWorker.
    • dotnet_code_quality.LC030.long_lived_types = MyApp.IAlwaysSingleton;MyApp.LongLivedBase treats 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