DI003

Captive Dependency

Singleton services capturing scoped or transient dependencies, including constructor injection, IEnumerable<T> collection captures, known scoped framework services such as IOptionsSnapshot<T>, typed HTTP clients registered with AddHttpClient<TClient>() / AddHttpClient<TClient,TImplementation>(), EF Core contexts and DbContextOptions<TContext> registrations from AddDbContext(...), AddDbContextFactory(...), AddDbContextPool(...), and AddPooledDbContextFactory(...) including service/implementation overload self-registrations, and high-confidence factory paths such as inline delegates, stable local delegate factories, method-group factories, GetServices<T>(), keyed resolutions, and ActivatorUtilities.CreateInstance(...) calls where DI still resolves a scoped or transient constructor parameter. A factory that creates and provably disposes its own scope (using var scope = sp.CreateScope();) stays quiet for resolutions through that scope when only derived values flow into the product — one-time scoped setup is not a captive — while an escaping resolved instance or an undisposed factory scope still reports.

Warning Default severity · Code fix: Yes

Why it matters

Lifetime mismatch can produce stale state, leaks, and thread-safety defects.

If one pupil keeps the shared class scissors all term, nobody else can use them when needed.

README problem example

services.AddScoped<IScopedService, ScopedService>();
services.AddSingleton<ISingletonService, SingletonService>();

public sealed class SingletonService : ISingletonService
{
    public SingletonService(IScopedService scoped) { }
}

README better pattern

services.AddScoped<ISingletonService, SingletonService>();

// or keep singleton and create scopes inside operations
public sealed class SingletonService : ISingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public SingletonService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public void Run()
    {
        using var scope = _scopeFactory.CreateScope();
        var scoped = scope.ServiceProvider.GetRequiredService<IScopedService>();
        scoped.DoWork();
    }
}

Yes. Rewrites explicit registration lifetimes when the registration syntax is local and unambiguous (for example AddSingleton, TryAddSingleton, keyed AddKeyedSingleton, inline factory registrations, and supported ServiceDescriptor forms). The rewrite only ever targets MEDI registration methods — user helpers whose names happen to contain a lifetime token are never renamed.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app warning case

public class BadSingletonWithScopedDependency
{
    private readonly IScopedService _scopedService;

    // DI003: Singleton 'BadSingletonWithScopedDependency' captures scoped dependency 'IScopedService'
    public BadSingletonWithScopedDependency(IScopedService scopedService)
    {
        _scopedService = scopedService;
    }

    public void DoWork() => _scopedService.DoWork();
}

Sample app safe pattern

public class GoodSingletonWithScopeFactory
{
    private readonly IServiceScopeFactory _scopeFactory;

    public GoodSingletonWithScopeFactory(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    public void DoWork()
    {
#pragma warning disable DI007 // Factory pattern - IServiceScopeFactory is allowed to resolve services
        using var scope = _scopeFactory.CreateScope();
        var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
        scopedService.DoWork();
#pragma warning restore DI007
    }
}

Nearby diagnostics

Other rules in this family

All 37 rules