This document mirrors the full rule guidance in the repository README so the same guidance is available in both places.
For the latest full rule content, see:
| ID | Title | Default Severity | Code Fix |
|---|---|---|---|
| DI001 | Service scope not disposed | Warning | Yes |
| DI002 | Scoped service escapes scope | Warning | Yes |
| DI003 | Captive dependency | Warning | Yes |
| DI004 | Service used after scope disposed | Warning | No |
| DI005 | Use CreateAsyncScope in async methods |
Warning | Yes |
| DI006 | Static IServiceProvider cache |
Warning | Yes |
| DI007 | Service locator anti-pattern | Info | No |
| DI008 | Disposable transient service | Warning | Yes |
| DI009 | Open generic captive dependency | Warning | Yes |
| DI010 | Constructor over-injection | Info | No |
| DI011 | IServiceProvider injection |
Info | No |
| DI012 | Conditional/duplicate registration misuse | Info | No |
| DI013 | Implementation type mismatch | Error | No |
| DI014 | Root provider not disposed | Warning | Yes |
| DI015 | Unresolvable dependency | Warning | Yes |
| DI016 | BuildServiceProvider misuse during registration | Warning | No |
| DI017 | Circular dependency | Warning | No |
| DI018 | Non-instantiable implementation type | Warning | No |
What it catches: IServiceScope instances created with CreateScope() or CreateAsyncScope() that are never disposed.
Why it matters: undisposed scopes can retain scoped and transient disposable services longer than expected, causing memory and handle leaks.
Explain Like I'm Ten: If you borrow a paintbrush and never wash it, it dries out and ruins the next project.
Problem:
public void Process()
{
var scope = _scopeFactory.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService<IMyService>();
svc.Run();
}Better pattern:
public void Process()
{
using var scope = _scopeFactory.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService<IMyService>();
svc.Run();
}Code Fix: Yes. Adds using / await using where possible.
What it catches: a service resolved from a scope that is returned or stored somewhere longer-lived, including scopes declared before a later using (scope) disposal block and the same patterns inside constructors, accessors, local functions, lambdas, and anonymous methods.
Why it matters: once the scope is disposed, that service may point to disposed state.
Explain Like I'm Ten: It is like taking an ice cube out of the freezer for later; by the time you need it, it has melted.
Problem:
public IMyService GetService()
{
using var scope = _scopeFactory.CreateScope();
return scope.ServiceProvider.GetRequiredService<IMyService>();
}Better pattern:
public void UseServiceNow()
{
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
service.Execute();
}Code Fix: Yes (suppression and acknowledgement options where direct refactor is not safe).
What it catches: singleton services capturing scoped or transient dependencies, including constructor injection and high-confidence factory paths such as inline delegates, method-group factories, keyed resolutions, and ActivatorUtilities.CreateInstance(...) without explicit constructor arguments.
Why it matters: lifetime mismatch can produce stale state, leaks, and thread-safety defects.
Explain Like I'm Ten: If one pupil keeps the shared class scissors all term, nobody else can use them when needed.
Problem:
services.AddScoped<IScopedService, ScopedService>();
services.AddSingleton<ISingletonService, SingletonService>();
public sealed class SingletonService : ISingletonService
{
public SingletonService(IScopedService scoped) { }
}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();
}
}Code Fix: Yes. Rewrites explicit registration lifetimes when the registration syntax is local and unambiguous (for example AddSingleton, keyed AddKeyedSingleton, and supported ServiceDescriptor forms).
What it catches: using a service after the scope that produced it has already ended, including 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.
Why it matters: leads to runtime disposal errors and brittle service behaviour.
Explain Like I'm Ten: It is like trying to turn on a torch after you removed the batteries.
Problem:
IMyService service;
using (var scope = _scopeFactory.CreateScope())
{
service = scope.ServiceProvider.GetRequiredService<IMyService>();
}
service.DoWork();Better pattern:
using (var scope = _scopeFactory.CreateScope())
{
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
service.DoWork();
}Code Fix: No. Usually needs manual refactor.
What it catches: CreateScope() used in async flows where async disposal is needed.
Why it matters: async disposables (IAsyncDisposable) may not be cleaned up correctly with sync disposal patterns.
Explain Like I'm Ten: If a machine needs a proper shutdown button, pulling the plug is not enough.
Problem:
public async Task RunAsync()
{
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
await service.ExecuteAsync();
}Better pattern:
public async Task RunAsync()
{
await using var scope = _scopeFactory.CreateAsyncScope();
var service = scope.ServiceProvider.GetRequiredService<IMyService>();
await service.ExecuteAsync();
}Code Fix: Yes. Rewrites scope creation/disposal pattern.
What it catches: IServiceProvider / IServiceScopeFactory / keyed provider stored in static fields or properties.
Why it matters: global provider state encourages service locator use and muddles lifetime boundaries.
Explain Like I'm Ten: Leaving the school master key in the corridor means anybody can open any door at any time.
Problem:
public static class Locator
{
public static IServiceProvider Provider { get; set; } = null!;
}Better pattern:
public sealed class Locator
{
private readonly IServiceProvider _provider;
public Locator(IServiceProvider provider)
{
_provider = provider;
}
}Code Fix: Yes. Removes static modifier in common cases.
What it catches: resolving dependencies via IServiceProvider inside app logic.
Why it matters: hides real dependencies, makes tests harder, and weakens architecture boundaries.
Explain Like I'm Ten: If every meal starts with "search the kitchen and see what turns up", dinner becomes chaos.
Problem:
public sealed class MyService
{
private readonly IServiceProvider _provider;
public MyService(IServiceProvider provider)
{
_provider = provider;
}
public void Run()
{
var dep = _provider.GetRequiredService<IDependency>();
dep.Execute();
}
}Better pattern:
public sealed class MyService
{
private readonly IDependency _dependency;
public MyService(IDependency dependency)
{
_dependency = dependency;
}
public void Run() => _dependency.Execute();
}Code Fix: No. This is usually architectural refactoring.
What it catches: transient services implementing IDisposable/IAsyncDisposable in risky patterns.
Why it matters: disposal ownership can become unclear and resources may be leaked.
Explain Like I'm Ten: Borrowing a bike every minute without returning the old one fills the whole bike shed.
Problem:
services.AddTransient<IMyService, DisposableService>();
public sealed class DisposableService : IMyService, IDisposable
{
public void Dispose() { }
}Better pattern:
services.AddScoped<IMyService, DisposableService>();
// or ensure explicit disposal ownership if transient is intentionalCode Fix: Yes. Suggests safer lifetime alternatives.
What it catches: 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.
Why it matters: every closed generic instance inherits the lifetime mismatch.
Explain Like I'm Ten: If the recipe is wrong at the top of the cookbook, every dish made from it comes out wrong.
Problem:
services.AddScoped<IScopedService, ScopedService>();
services.AddSingleton(typeof(IRepository<>), typeof(Repository<>));
public sealed class Repository<T> : IRepository<T>
{
public Repository(IScopedService scoped) { }
}Better pattern:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));DI009 follows the single likely activation constructor the container can actually use. Optional/default-value parameters are treated as activatable during that selection, and ambiguous equally-greedy constructor sets stay silent instead of guessing.
Code Fix: Yes. Can adjust lifetime for open generic registrations.
What it catches: constructors with too many meaningful dependencies.
Why it matters: often signals a class with too many responsibilities.
Explain Like I'm Ten: If one backpack needs ten straps to carry, it is probably trying to hold too much at once.
Problem:
public sealed class ReportingService
{
public ReportingService(
IDep1 dep1,
IDep2 dep2,
IDep3 dep3,
IDep4 dep4,
IDep5 dep5)
{
}
}Better pattern: split into focused collaborators and inject smaller abstractions.
For normal type registrations, DI010 evaluates the constructor(s) the container could realistically activate instead of every accessible constructor. It also covers straightforward factory registrations that directly return new MyService(...) or ActivatorUtilities.CreateInstance<MyService>(sp), while staying conservative on more dynamic factories.
By default, DI010 reports when a constructor has more than 4 meaningful dependencies. It ignores primitives/value types, optional parameters, provider-plumbing types already covered by DI011, and common framework abstractions such as ILogger<T>, IOptions<T>, and IConfiguration.
Configure the threshold in .editorconfig:
[*.cs]
dotnet_code_quality.DI010.max_dependencies = 5Code Fix: No. Design decision required.
What it catches: constructor injection of IServiceProvider, IServiceScopeFactory, or IKeyedServiceProvider in normal services.
Why it matters: this commonly enables hidden runtime resolution and service locator behaviour.
Explain Like I'm Ten: Asking for a giant "surprise box" each time instead of a known tool means no one knows what you actually need.
Problem:
public sealed class MyService
{
public MyService(IServiceProvider provider) { }
}Better pattern: inject concrete dependencies directly.
Code Fix: Yes. Adds a missing self-binding registration only when DI015 can prove a single direct constructor dependency is a concrete, non-keyed type and the registration call is local and unambiguous.
Fixable case:
public sealed class MissingDependency { }
public sealed class MyService : IMyService
{
public MyService(MissingDependency missing) { }
}
services.AddScoped<IMyService, MyService>();DI015 can offer:
services.AddScoped<MissingDependency>();
services.AddScoped<IMyService, MyService>();Not auto-fixable: abstractions/interfaces, keyed dependencies, multiple missing dependencies, transitive-only missing leaves, ServiceDescriptor registrations, and factory-rooted diagnostics.
Known exceptions in this rule: factory-style types, middleware Invoke/InvokeAsync paths, hosted services, and endpoint filter factories.
What it catches:
TryAdd*calls after anAdd*already registered that service.- Duplicate
Add*registrations where later entries override earlier ones.
DI012 also follows the same IServiceCollection flow across local aliases and source-defined helper/local-function wrappers, while treating opaque helper boundaries conservatively instead of guessing at registration order.
Why it matters: registration intent becomes unclear and behaviour differs from what readers expect.
Explain Like I'm Ten: Writing your name on the same seat twice does not get you two seats; one note just replaces the other.
Problem:
services.AddSingleton<IMyService, ServiceA>();
services.TryAddSingleton<IMyService, ServiceB>(); // ignored
services.AddSingleton<IMyService, ServiceA>();
services.AddSingleton<IMyService, ServiceB>(); // overrides ABetter pattern: decide and signal intent clearly: TryAdd* first, or explicit override with comments/tests.
Code Fix: No.
What it catches: invalid typeof service/implementation pairs that compile but fail at runtime.
Why it matters: service activation throws at runtime (ArgumentException/InvalidOperationException depending on path).
Explain Like I'm Ten: A round plug will not fit a square socket just because both are on your desk.
Problem:
public interface IRepository { }
public sealed class WrongType { }
services.AddSingleton(typeof(IRepository), typeof(WrongType));Better pattern:
public sealed class SqlRepository : IRepository { }
services.AddSingleton(typeof(IRepository), typeof(SqlRepository));Code Fix: No.
What it catches: root providers from BuildServiceProvider() that are never disposed.
Why it matters: singleton disposables at root scope may never be cleaned up.
Explain Like I'm Ten: Locking the front door but leaving all the taps running still wastes the whole house.
Problem:
var services = new ServiceCollection();
var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService<IMyService>();Better pattern:
using var provider = services.BuildServiceProvider();
var service = provider.GetRequiredService<IMyService>();Code Fix: Yes. Adds disposal pattern where safe.
What it catches: registered services with direct or transitive constructor/factory dependencies that are not registered (including keyed and open-generic paths).
Why it matters: runtime activation fails when DI tries to create the service.
Explain Like I'm Ten: Planning to build a kite without string means the build fails when you start.
Problem:
public interface IMissingDependency { }
public interface IMyService { }
public sealed class MyService : IMyService
{
public MyService(IMissingDependency missing) { }
}
services.AddSingleton<IMyService, MyService>();Better pattern:
public sealed class MissingDependency : IMissingDependency { }
services.AddScoped<IMissingDependency, MissingDependency>();
services.AddSingleton<IMyService, MyService>();Code Fix: No.
By default, DI015 assumes common host-provided framework services (logging/options/configuration) are available. Disable that assumption for stricter analysis:
[*.cs]
dotnet_code_quality.DI015.assume_framework_services_registered = falseDI015 is intentionally conservative to keep false positives low:
- Source-visible
IServiceCollectionwrappers are expanded before DI015 reports missing registrations. - Dependency cycles are treated as resolvable.
- Factory registrations without inspectable dependency paths are treated as resolvable.
GetService(...)and dynamic keyed resolutions are treated as optional/unknown.- If an earlier opaque or external wrapper could have registered services on the same
IServiceCollectionflow, DI015 stays silent instead of speculating. - If any effective candidate registration is backed by an opaque factory, DI015 stays silent instead of speculating.
What it catches: BuildServiceProvider() calls while composing registrations (for example in ConfigureServices, IServiceCollection extension registration methods, registration lambdas, or builder-style .Services helper flows).
Why it matters: building a second provider during registration can duplicate singleton instances and produce lifetime inconsistencies.
Explain Like I'm Ten: If you set up a second classroom register halfway through, children can end up counted twice and rules become muddled.
Problem:
public static IServiceCollection AddFeature(this IServiceCollection services)
{
var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IMyOptions>();
return services;
}Better pattern:
public static IServiceCollection AddFeature(this IServiceCollection services, IMyOptions options)
{
// Use provided dependencies/options without creating a second container
return services;
}Code Fix: No.
DI016 is intentionally conservative to reduce false positives:
- It only reports symbol-confirmed DI
BuildServiceProvider()calls in registration contexts. - It does not report provider-factory methods that intentionally return
IServiceProvider. - It recognizes assignable
IServiceCollectionabstractions and same-boundary helper/alias flows from.Services, but it does not warn on standalone top-levelnew ServiceCollection()composition roots.
What it catches: constructor-injection cycles such as A -> B -> A, including longer transitive loops.
Why it matters: the default DI container cannot resolve circular constructor graphs and will fail at runtime when the service is activated.
Explain Like I'm Ten: If two people each wait for the other to hand over the key first, the door never opens.
Problem:
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IPaymentService, PaymentService>();
public sealed class OrderService : IOrderService
{
public OrderService(IPaymentService payment) { }
}
public sealed class PaymentService : IPaymentService
{
public PaymentService(IOrderService order) { }
}Better pattern: break the cycle by moving shared logic into a third collaborator or by changing the dependency direction so each service has an acyclic constructor graph.
Code Fix: No. Breaking dependency cycles is a design change.
What it catches: registrations whose implementation type cannot be constructed by the DI container, such as abstract classes, interfaces, static classes, or concrete classes with no public constructors.
Why it matters: these registrations compile, but fail at runtime when the container tries to activate the service.
Explain Like I'm Ten: Writing a ghost on the class register does not mean someone can actually show up for class.
Problem:
public interface IMyService { }
public sealed class BadPrivateCtorService : IMyService
{
private BadPrivateCtorService() { }
}
services.AddSingleton<IMyService, BadPrivateCtorService>();DI018 also reports abstract classes, interfaces, and static classes used as implementation types.
Better pattern:
public sealed class GoodConcreteService : IMyService { }
services.AddSingleton<IMyService, GoodConcreteService>();Code Fix: No.