DI019

Scoped Service Resolved From Root Provider

Scoped services, known scoped framework services such as IOptionsSnapshot<T>, EF Core contexts from AddDbContext(...), AddDbContextFactory(...), AddDbContextPool(...), and AddPooledDbContextFactory(...) including service/implementation overload self-registrations, or services whose activation graph reaches a scoped service, resolved from a root IServiceProvider such as ASP.NET Core app.Services, ASP.NET test-host factory.Services / server.Services, Generic Host host.Services, nullable root-provider surfaces such as app.Services!, or a provider returned by BuildServiceProvider(). Root-provider aliases also stay classified through ?? throw guards and conditional expressions whose two result arms are proven root through path-stable declarations or straight-line writes. Provider declarations and assignments are collected in source order, path stability propagates through copied aliases, later unclassified, ??=, deconstruction, and ref/out writes invalidate older provider facts. Write facts become visible only after right-hand-side, initializer, or argument evaluation, and nested mutation events are processed before their enclosing write, so resolutions and alias copies observe the provider state at that runtime point. Assignments in the always-evaluated left operand of &&, ||, or ??, in a ternary's always-evaluated condition, and in the governing expression of a switch statement or switch expression retain path stability; matching right-operand writes, ternary result-arm writes, and writes in switch sections or result arms stay conservative because execution can skip them. Nested ternary-arm, short-circuit-right, and null-conditional WhenNotNull writes remain conditional even inside a switch governing expression, while a write in the null-conditional receiver remains definite. Merely binding or retargeting a ref local preserves the referents' facts; source-positioned mappings ensure later writes follow every possible storage active at that point across conditional or unconditional retargeting and ref-conditional local, by-reference argument, or lvalue targets, while reads use only the mapping active at their position and classify the alias only when every possible storage agrees. Writes through aliases with multiple possible referents invalidate every candidate storage rather than claiming each one definitely received the new value. Forward or backward goto edges cannot make path-dependent facts stable. Field/property facts never qualify because source position cannot prove cross-method execution; deferred lambda, LINQ-query, and local-function hazards remain conservative for captured outer storage, while locals and parameters owned by the deferred boundary retain ordinary path stability for declarations and straight-line writes. Control flow outside that owning boundary does not alter the path executed inside it. Other control-flow-dependent, mixed root/scoped, and unknown arms stay conservative. Both ordinary extension syntax and direct static calls through the exact framework ServiceProviderServiceExtensions and ServiceProviderKeyedServiceExtensions types are analyzed, including reordered named arguments; same-named user extensions stay silent.

Warning Default severity · Code fix: Yes

Why it matters

The default container's scope validation is designed to prevent scoped services from being resolved directly or indirectly from the root provider. Resolving them from root can fail at runtime or accidentally stretch scoped state to application lifetime.

A classroom pass only works for one lesson. Taking it home for the whole year breaks the rules.

README problem example

var app = builder.Build();
var db = app.Services.GetRequiredService<MyDbContext>();

README better pattern

var app = builder.Build();
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();

Yes. Offers to wrap ordinary extension-form resolutions in a using declaration or block with a new scope. Direct static-call syntax reports without a code fix because rewriting the declaring type as a provider receiver would not compile.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app root-provider warning

    public static void BuildRootScopedResolutionExample()
    {
        var services = new ServiceCollection();
        services.AddScoped<IScopedService, ScopedService>();
        services.AddTransient<BadRootResolvedRunner>();

        using var provider = services.BuildServiceProvider();

        // DI019: Resolves a scoped service directly from the root provider.
        provider.GetRequiredService<IScopedService>();

        // DI019: Resolves a transient graph that reaches a scoped service from the root provider.
        provider.GetRequiredService<BadRootResolvedRunner>();
    }

Sample app scoped-provider pattern

    public static void GoodScopedResolutionExample()
    {
        var services = new ServiceCollection();
        services.AddScoped<IScopedService, ScopedService>();

        using var provider = services.BuildServiceProvider();
        using var scope = provider.CreateScope();

        var service = scope.ServiceProvider.GetRequiredService<IScopedService>();
        service.DoWork();
    }

Related guides

  • No problem-guide pages point here yet.

Nearby diagnostics

Other rules in this family

All 37 rules