Rules

Rule index

All 37 analyzer rules with rule summaries, README examples, and extracted sample-app snippets. 17 ship a code fix.

DI001

Service Scope Not Disposed

IServiceScope instances created with CreateScope() or CreateAsyncScope() that are never disposed, including scopes whose only disposal call is hidden behind a conditional branch, or behind a switch section, loop, or catch block that does not also contain the creation, or after a branch exit that can bypass shared cleanup. Create-and-dispose within the same loop iteration, switch section, or catch clause — the per-message worker shape — stays quiet, but a continue/break that skips the dispose, or a yield return/yield break that can strand the scope in a never-resumed iterator, still reports. DI001 recognizes predeclared nullable scope locals assigned conditionally when a later conditional-access, non-null-guarded, same-branch pre-exit, or finally disposal reliably closes ownership, and it treats directly returned scopes as caller-owned even through simple casts or conditional return arms. Reassignment leaks and loop-created scopes that need per-iteration disposal still report.

Warning Code fix: Yes

DI002

Scoped Service Escapes Scope

A service resolved from a tracked IServiceScope with a known scoped registration that is returned or stored beyond that scope. The tracked scope shapes are direct CreateScope()/CreateAsyncScope() calls and existing scope locals later disposed in the same executable boundary. The rule follows the direct provider aliases it can prove and works inside constructors, accessors, local functions, lambdas, and anonymous methods.

Warning Code fix: Yes

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 Code fix: Yes

DI004

Service Used After Scope Disposed

Using a service after the scope that produced it has already ended, including scoped collections from GetServices<T>() enumerated after disposal, explicit Dispose() / DisposeAsync() (including scope?.Dispose() for scope locals), wrapped use receivers such as service!.DoWork() and ((IService)service).DoWork(), services resolved from a predeclared scope variable later disposed via using (scope), and the same patterns inside constructors, accessors, local functions, lambdas, and anonymous methods. Uses in branches mutually exclusive with the disposal — whether the dispose is explicit or a using statement/declaration — stay quiet, and out arguments are writes rather than uses (the rewritten local is fresh afterwards), while ref arguments still report.

Warning Code fix: Yes

DI005

Use `CreateAsyncScope` in Async Methods

CreateScope() used in async flows where async disposal is needed and CreateAsyncScope() is available, including async methods, lambdas, local functions, anonymous methods, and top-level programs that use await. Detection covers regular member access (_scopeFactory.CreateScope()), parameterless IServiceScope CreateScope() methods on concrete IServiceScopeFactory implementations, and conditional-access receivers (_scopeFactory?.CreateScope(), _provider?.CreateScope()) alike.

Warning Code fix: Yes

DI006

Static `IServiceProvider` Cache

IServiceProvider / IServiceScopeFactory / keyed provider stored in static fields or properties, including common wrappers (Lazy<T>, Task<T>, ValueTask<T>, Func<T>, AsyncLocal<T>, ThreadLocal<T>), mutable/immutable/frozen dictionary value caches, recursive dictionary values such as Dictionary<string, Lazy<IServiceProvider>>, and simple holder types that only wrap a provider.

Warning Code fix: Yes

DI007

Service Locator Anti-Pattern

Resolving dependencies via IServiceProvider inside app logic, including non-generic resolution calls that pass a local Type alias initialized from typeof(...).

Info Code fix: No

DI009

Open Generic Captive Dependency

Open generic singleton registrations that depend on shorter-lived services, including common registration-shape variants such as TryAddSingleton(...), ServiceDescriptor.Singleton(...), keyed open-generic singleton registrations, and IEnumerable<T> constructor captures where the element service is shorter-lived.

Warning Code fix: Yes

DI011

`IServiceProvider` Injection

Constructor injection of IServiceProvider, IServiceScopeFactory, or IKeyedServiceProvider in normal services.

Info Code fix: No

DI013

Implementation Type Mismatch

Invalid service/implementation pairs that compile but fail at runtime, including generic, typeof(...), keyed, named-argument, and ServiceDescriptor registrations. Closed-type compatibility follows CLR assignability — identity, reference, boxing, and TNullable<T> — so implicit numeric conversions and user-defined operators are not treated as a valid implementation binding.

Error Code fix: Yes

DI014

Root Service Provider Not Disposed

Root providers from BuildServiceProvider() that are never disposed, including local providers whose only manual disposal is conditional, catch-only, after reassignment to another provider, or after repeated creation inside a loop. Straight-line explicit disposal, standard Dispose() to Dispose(true) cleanup, and caller-owned return flows are accepted even when the BuildServiceProvider() result is parenthesized, same-instance cast, null-forgiven, selected by a ternary arm, or supplied by a null-coalescing operand — including a provider stored in a local and returned later (ownership transfer), and create-and-dispose within the same loop iteration, switch section, or catch clause (a continue/break that skips the dispose still reports). A using declaration or statement proves cleanup only when that same provider instance reaches its resource expression. User-defined conversions remain reportable because they may produce a different instance, including a disposable wrapper selected by a coalesce inside using.

Warning Code fix: Yes

DI015

Unresolvable Dependency

Registered services with direct or transitive constructor/factory dependencies that are not registered (including keyed and open-generic paths).

Warning Code fix: Yes

DI016

BuildServiceProvider Misuse

BuildServiceProvider() calls while composing registrations (for example in ConfigureServices, IServiceCollection extension registration methods, registration lambdas, or builder-style .Services helper flows), whether written as reduced extension syntax (services.BuildServiceProvider()) or as a direct static call (ServiceCollectionContainerBuilderExtensions.BuildServiceProvider(builder.Services)).

Warning Code fix: No

DI017

Circular Dependency

High-confidence activation cycles such as A -> B -> A, including longer transitive loops through constructors, explicit GetRequiredService / GetRequiredKeyedService factory calls, ActivatorUtilities factory construction, keyed-service inheritance, open-generic registrations, exact closed registrations that override open-generic fallbacks, and registered IEnumerable<T> elements. It analyzes only reachable service-registration flows and mirrors the default container's constructor-set rule: equivalent reordered greedy constructors expose the same cycle, while a greediest constructor whose resolved service identifiers (type plus key) are not a superset of every other resolvable constructor stays silent as ambiguous.

Warning Code fix: No

DI018

Non-Instantiable Implementation Type

Registrations whose implementation type cannot be constructed by the DI container, such as abstract classes, interfaces, static classes, delegate types registered without a factory, default structs and enums, or concrete classes with no public constructors.

Warning Code fix: No

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 Code fix: Yes

DI020

Middleware Captures Scoped Service In Constructor

Scoped services captured by the constructor of a conventional middleware class — both directly (a scoped parameter) and transitively (a parameter whose activation graph reaches a scoped service). Middleware registrations are recognized in reduced extension form (app.UseMiddleware<T>()) and in direct framework static form (UseMiddlewareExtensions.UseMiddleware<T>(app) / UseMiddlewareExtensions.UseMiddleware(app, typeof(T))), with explicit activation arguments matched to constructor parameters. A stable local reference-type array (such as object[] or string[]) initialized by a fixed array creation is expanded positionally just like the framework's params object[] call; reassigned, external, uninitialized, and otherwise dynamic arrays remain unproven and silent. The adversarial boundary is recorded in docs/adversarial/DI020.md.

Warning Code fix: No

DI021

Non-Thread-Safe Service Shared Across Concurrent Handler Invocations

A documented non-thread-safe service (EF Core DbContext and derived contexts, DbConnection/DbCommand/DbTransaction/DbDataReader and their interfaces, IDbContextTransaction, HttpContext) created or resolved once and then captured — through a field, a closure over an outer local, or an enclosing method parameter — into a handler that a framework invokes concurrently: ServiceBusProcessor/ServiceBusSessionProcessor message and error handlers, EventProcessorClient event handlers, RabbitMQ EventingBasicConsumer.Received/AsyncEventingBasicConsumer.Received/ReceivedAsync consumer handlers (instance-correlated through the consumer's own factory/connection/channel chain: proven ConsumerDispatchConcurrency above 1 warns, proven 1 or a fresh default factory stays silent, untraceable chains stay config-gated; fallback constants must bind to the real RabbitMQ property), System.Threading.Timer callbacks with a finite period, System.Timers.Timer.Elapsed, Parallel.For/ForEach/ForEachAsync/Invoke bodies, PLINQ ForAll bodies (sequential only when WithDegreeOfParallelism(1) is proven on the query chain), TPL Dataflow ActionBlock/TransformBlock/TransformManyBlock delegates (sequential by default; reported when MaxDegreeOfParallelism is provably above 1, config-gated DI022 when the options are unprovable), and EventProcessor<TPartition> batch/error overrides (the override body is the handler; partitions run concurrently). Resolving from a long-lived scope captured from outside the handler is reported too — it hands the same instance to every concurrent invocation. Both generic requests and exact framework non-generic IServiceProvider.GetService(typeof(T)) / GetRequiredService(typeof(T)) requests participate; direct-static calls bind the provider by declared parameter only for exact framework extension containers, and concrete provider implementations bind the System.Type contract parameter regardless of its source name. Generic service-looking calls must bind to the exact framework extension symbol; same-named user-defined generic helpers remain silent. The adversarial boundary is recorded in docs/adversarial/DI021.md. Built-in identity, reference, and boxing conversions remain transparent while tracing provider origins, preserving coverage for captured value-type and IServiceProvider-constrained providers; cyclic constraint graphs in temporarily invalid source are bounded by symbol identity. Runtime Type values, user-defined conversions on the requested type or provider receiver, and user-defined same-named helpers remain conservative and silent.

Warning Code fix: Yes

DI022

Service Instance Reused Across Handler Invocations

Two tiers. First, the same capture shape as DI021 on a sink whose concurrency is controlled by a configuration knob that cannot be proven at compile time — canonically ServiceBusProcessor where MaxConcurrentCalls comes from configuration or is left at its default of 1, and RabbitMQ consumers (EventingBasicConsumer/AsyncEventingBasicConsumer) where ConsumerDispatchConcurrency lives on the ConnectionFactory several hops from the consumer; a constant above 1 on the actual SDK property upgrades the report to DI021, while unrelated same-named user properties do not. Second, the scoped-lifetime tier: a service outside the non-thread-safe catalog whose effective registration is scoped, captured into any concurrently-invoked handler — the capture itself is the lifetime violation, so the report stays Info regardless of the sink's knob. Singleton-registered and unregistered captures stay silent.

Info Code fix: Yes

DI023

Fire-and-Forget Background Work Captures a Scope

A using scope, a local bound to its ServiceProvider, or any local resolved from it, captured by background work started with Task.Run or TaskFactory.StartNew whose task is thrown away — an expression statement, a _ = discard, or a finite/cancelable Wait(...) whose Boolean result is stored or returned while the task may continue.

Warning Code fix: No

DI024

Hosted Service Creates Scope Outside Execution Loop

Two tiers. First, a BackgroundService.ExecuteAsync override or IHostedService/IHostedLifecycleService start method that creates an IServiceScope once before its long-running execution loop (while (!token.IsCancellationRequested), compound cancellation conditions, while (true), for (;;), System.Threading.PeriodicTimer.WaitForNextTickAsync loops (same-named custom timer methods do not qualify), and channel-consumer loops — await foreach over ChannelReader<T>.ReadAllAsync(...) or while (await reader.WaitToReadAsync(...)), including channel loops nested inside an outer cancellation loop when the scope is created per outer iteration but spans the unbounded inner drain; ConfigureAwait(...)/WithCancellation(...) wrappers on any of the awaited shapes are peeled before gating) and uses it inside the loop — directly, through a service resolved from it before the loop, or through a provider alias local (var sp = scope.ServiceProvider;) used inside the loop. The same helper-local analysis follows one-hop, directly invoked private helpers declared on the same type; field candidates stay confined to true hosted entry points. Generic resolutions and the framework's direct-typeof(T) non-generic GetService/GetRequiredService forms participate, including keyed GetKeyedService/GetRequiredKeyedService calls whose service key is compile-time known, plus casted and null-forgiving results; runtime Type values, dynamic keys, and user-defined same-named methods remain unproven. Compound conditions are evaluated conservatively: nested ! operators are reduced by polarity, every && operand must be long-running because any operand can bound the loop, while one long-running || operand is sufficient; negated cancellation combinations use De Morgan semantics. Declare-then-assign locals (IServiceScope? scope = null; try { scope = factory.CreateScope(); while (...) ... } finally { scope?.Dispose(); } — the try/finally ownership pattern) qualify via their pre-loop assignment: the last direct pre-loop write wins, so a creation makes the candidate and a null/default clear (or an unrecognized value) kills it. Second, a service whose effective registration is provably scoped, resolved once before the loop from any provider and reused across iterations. Both tiers also cover fields: a scope (or resolved service) stored in a field qualifies when every assignment to the field is the expected shape and every assignment site is a field initializer, a constructor, or a hosted execution method (BackgroundService.StartAsync overrides included); partial types are analyzed across all declarations. Reported at the CreateScope/CreateAsyncScope or service-resolution call with the loop as an additional location.

Warning Code fix: No

DI025

Event Subscription On Longer-Lived Publisher Without Unsubscribe

A transient- or scoped-registered service that subscribes (+=) an instance-capturing handler — an instance method group, a this-capturing lambda, or a stored instance-bound delegate field — to an event on a longer-lived publisher and never unsubscribes. Subscriber lifetime is evaluated per effective service slot: a shorter-lived slot keeps the leak report even when the same implementation also has a singleton slot, while an overridden same-slot transient descriptor does not. Longer-lived publishers are injected dependencies whose registration is provably singleton — closed registrations preferred, open-generic singleton registrations matched for constructed injections — via a constructor parameter or a field/property assigned only from a constructor parameter, and static events. Identity and reference casts preserve that proof, so ((IBaseBus)_bus).Changed += H reports for direct injected receivers and already-proven stable chains. Chained receivers (_host.Bus.Changed += H) report when the publisher is a stable projection of an injected root: the lifetime proof anchors on the chain root's registration, and every intermediate segment must be a readonly field, a get-only auto-property, or a getter returning one, with interface segments proven through the root's registered implementation types. Because C# forbids assigning another type's field-like event, the cross-type delegate leak lives on a delegate-typed field or property of the publisher instead: _bus.Handlers += OnMessage and the equivalent self-assignment _bus.Handlers = (EventHandler)Delegate.Combine(_bus.Handlers, OnMessage) report identically to an event +=, with a mirrored Delegate.Remove self-assignment recognized as the matching unsubscription. A -= written with a different lambda instance is recognized as the classic no-op unsubscribe bug: the subscription still reports and the diagnostic points at the ineffective -=.

Warning Code fix: Yes

DI026

Event Subscription On Scoped Publisher Without Unsubscribe

The scope-bounded tier of DI025: a transient-registered service subscribes an instance-capturing handler to an event on a scoped registered publisher — the receiver, identity/reference-cast, handler, and unsubscription proofs are exactly DI025's — and never unsubscribes. Publisher lifetime resolution follows the same rules (most conservative registration wins, closed registrations preferred over open-generic fallbacks, keyed-only registrations excluded), so a publisher registered both scoped and singleton reports DI026: only the scope-bounded claim is provable.

Info Code fix: Yes

DI027

Rx Subscription On Longer-Lived Observable Without Dispose

The Rx twin of DI025. IObservable<T>.Subscribe(...) returns an IDisposable token that unsubscribes the observer when disposed, so there is no -= to prove missing — the leak proof inverts to a discarded token. A transient or scoped registered service subscribes an instance-capturing handler (method group, this-capturing lambda, or stored delegate) to an observable exposed by a longer-lived publisher — an injected singleton dependency, or a scoped publisher shared by a transient subscriber — and discards the returned token. The observable is reached through DI025's classified receivers (an injected member proven ctor-assigned, a constructor parameter, or a stable chained projection such as _source.Ticks), and publisher lifetime resolution follows the same rules (most conservative registration wins, closed registrations preferred over open-generic fallbacks, keyed-only registrations excluded). Matching is FQN-light: any method named Subscribe returning System.IDisposable, invoked on a System.IObservable<T> receiver, so System.Reactive, community Rx, and hand-rolled extensions all bind.

Warning Code fix: No

DI028

Discarded Callback Registration On A Longer-Lived Source

The third member of the DI025/DI027 family. Where DI025 proves a missing -= and DI027 proves a discarded Subscribe token, DI028 covers every remaining way .NET hands out a callback registration: IOptionsMonitor<T>.OnChange, CancellationToken.Register / UnsafeRegister, ChangeToken.OnChange, IChangeToken.RegisterChangeCallback, and CancellationTokenSource.CreateLinkedTokenSource. A transient or scoped registered service registers a callback on a longer-lived source — an injected singleton options monitor, IHostApplicationLifetime.ApplicationStopping, a token from a singleton-held CancellationTokenSource, a configuration reload token — and discards the registration that would detach it.

Warning Code fix: No

DI029

HttpClient Lifetime Misuse

Two opposite lifetime errors on the same connection pool. Socket exhaustion — a registered service constructs new HttpClient(...) on a per-invocation path (a method, accessor, lambda, any loop body, or the constructor of a transient service). Stale DNS — an HttpClient is handed to the container as a singleton (AddSingleton<HttpClient>, AddSingleton(new HttpClient()), a singleton ServiceDescriptor, or a keyed singleton) or held in a static field or property.

Warning Code fix: No

DI030

Unbounded Singleton Or Static Cache

Two shapes of a store that never shrinks. Unbounded growth — a private field of a concrete mutable collection (ConcurrentDictionary<,>, Dictionary<,>, List<>, HashSet<>, Queue<>, ConcurrentBag<>, ConcurrentQueue<>) that is static or owned by a singleton-registered service, written on a per-invocation path with a key derived from request input, where nothing in the declaring type ever removes, clears, drains, or size-checks it. Unbounded cache entries — an IMemoryCache.Set / GetOrCreate / CreateEntry call with an unbounded key and neither an expiration nor a Size, in a compilation whose cache has no SizeLimit.

Info Code fix: No

DI032

Service Implements Only IAsyncDisposable

A service the container creates — a plain type registration at any lifetime — whose implementation implements IAsyncDisposable but not IDisposable.

Warning Code fix: No

DI034

HttpContext Used in Fire-and-Forget Background Work

An HttpContext value — a parameter, local, or field of that type — or a read of IHttpContextAccessor.HttpContext, inside background work started with Task.Run or TaskFactory.StartNew whose task is thrown away or observed only through a finite/cancelable Wait(...) Boolean result.

Warning Code fix: No

DI035

Non-Thread-Safe Service Shared Across a Fan-Out

A documented non-thread-safe service — an EF Core DbContext or a derived context, IDbContextTransaction, or an ADO.NET connection, command, transaction, or reader — declared outside a Task.WhenAll projection and used inside every one of its tasks. This includes a service created once per outer SelectMany group and then shared by the inner tasks flattened into the same WhenAll.

Warning Code fix: No

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 Code fix: No

DI037

Un-awaited Task Escapes The Scope That Created It

A task started on a service resolved from a using service scope and then allowed to leave that scope without being awaited — returned to the caller, discarded with _ = or as a bare statement, assigned to a field or property, or collected into a list declared outside the scope to be awaited after it ends.

Warning Code fix: No