DI036

Registration Added After The Provider Was Built

A call that mutates an IServiceCollectionAddSingleton, TryAddScoped, Configure, Replace, Add(descriptor), any AddXxx/TryAddXxx extension — executed after a provider was already built from that same collection in the same method. The build is either BuildServiceProvider() on the collection or Build() on a host or web-application builder whose Services property *is* that collection, which covers the minimal-API shape var app = builder.Build(); builder.Services.AddSingleton<...>();.

Warning Default severity · Code fix: No

Why it matters

BuildServiceProvider copies the descriptor list into the provider it returns, and a host builder does the same when it builds. Every mutation after that point writes into a collection nothing reads again. There is no error and no warning at run time — the service either fails to resolve with InvalidOperationException: No service for type 'X' has been registered, or silently resolves to the registration the build did capture, so a Replace or a test-time override appears to be ignored. It is a favourite failure of integration-test fixtures and of Program.cs files where a late builder.Services line drifts below builder.Build().

The photo was already taken. Waving after the shutter clicks does not put you in the picture.

README problem example

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IReporting, Reporting>();

var app = builder.Build();

builder.Services.AddSingleton<IAudit, Audit>();  // DI036: the app never sees this

README better pattern

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IReporting, Reporting>();
builder.Services.AddSingleton<IAudit, Audit>();

var app = builder.Build();

No — the repair is a choice between moving the registration above the build and moving the build below it, and only the author knows whether anything between the two depends on the provider already existing.

Guardrails

When DI036 stays silent

The rule reports only what it can prove, and the proof is execution order inside one code block rather than source order alone. The build must be *unconditional* relative to the registration — a build nested in an if, a ternary arm, the right side of &&/||/??, or a switch arm may never have run, so it proves nothing — and it must come first in the shared statement list. A later build on the same collection cancels the finding outright, because that provider does see the registration; that keeps the probe-then-rebuild idiom silent, including when the rebuild is chained onto the registration itself (services.AddSingleton<T>().BuildServiceProvider()). A build inside a lambda, a local function, or a query clause cancels too, since a deferred context can run whenever its consumer chooses. A build and a registration that share an enclosing loop are skipped, because the next iteration builds again, and any goto in the code block disables the rule there.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app registration-after-build note

        public static IServiceProvider ConfigureBad()
        {
            var services = new ServiceCollection();
            var provider = services.BuildServiceProvider();

            // DI036: the provider already holds its own copy of the descriptor list, so this
            // audit trail never resolves from it.
            services.AddSingleton<IAuditTrail, AuditTrail>();

            return provider;
        }

Sample app build-last registration pattern

        public static IServiceProvider ConfigureGood()
        {
            var services = new ServiceCollection();
            services.AddSingleton<IAuditTrail, AuditTrail>();

            // Building last means every registration reaches the container.
            return services.BuildServiceProvider();
        }

Related guides

  • No problem-guide pages point here yet.

Nearby diagnostics

Other rules in this family

All 37 rules