Diagnosing ASP.NET Core Dependency Injection Failures: A Field Guide
Map each ASP.NET Core DI failure — missing registrations, scoped-from-singleton captures, circular dependencies, empty IEnumerable injections — to the check that confirms it and the fix that actually matches.
07 Aug 2025, 06:46 UTC

Your app starts fine, then the first request to a controller throws InvalidOperationException: No service for type 'MyApp.Services.IOrderService' has been registered. Or worse: it works in Development but fails in Production, or it silently returns stale data across requests. These are the classic failure modes of the built-in ASP.NET Core dependency injection (DI) container, and each one has a recognizable signature. This guide maps each symptom to its cause, the check that confirms it, and the fix that matches.
Everything below applies to the built-in Microsoft.Extensions.DependencyInjection container used by ASP.NET Core 6 through 8 and later. Third-party containers (Autofac, Lamar) share the concepts but differ in error messages.
Recognize the symptom before touching registrations
DI failures fall into five buckets. Identifying which one you have saves you from shotgun fixes like promoting everything to singleton.
| Symptom | Likely cause | First check |
|---|---|---|
No service for type 'X' has been registered | Missing registration, or interface/implementation mismatch | Search Program.cs for the exact type in the message |
Cannot consume scoped service ... from singleton | Scoped service captured by a singleton | Trace the constructor chain from the singleton |
App hangs or StackOverflowException at first resolution | Circular constructor dependency | Draw the constructor graph of the two services |
Injected IEnumerable<T> is empty, no exception | No T implementations registered | Count registrations for T |
| Works, but data leaks between requests or users | Scoped service effectively cached by a singleton | Check lifetimes of everything in the dependency chain |
Check 1: Reproduce the failure outside the web host
Before changing anything, reproduce the exact exception in isolation. Build a ServiceProvider with the same registrations in a console test or unit test and resolve the root service:
// Run in a test project or a scratch console app.
var services = new ServiceCollection();
// Copy the registrations from Program.cs here.
services.AddScoped<IOrderService, OrderService>();
services.AddSingleton<ReportCache>();
using var provider = services.BuildServiceProvider(
new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
// Resolve the service that fails in the real app.
var root = provider.GetRequiredService<ReportCache>();ValidateOnBuild = true forces the container to construct every registered service at startup, so missing registrations surface immediately instead of on the first request. ValidateScopes = true catches scoped-from-singleton captures. Both are enabled automatically in the Development environment by the default host builder; running this snippet yourself gives you the same checks on demand, in any environment.
Check 2: Missing registration or interface mismatch
The most common failure. The exception names the type the container could not find, so read it literally: if it says No service for type 'IOrderService', the problem is the interface registration, not the class. Two frequent variants:
- You registered the concrete type (
services.AddScoped<OrderService>()) but the constructor asks forIOrderService. Register the mapping:services.AddScoped<IOrderService, OrderService>(). - You registered
IOrderServiceto the wrong implementation, or registered it in a differentIServiceCollectionextension method that never gets called. Verify the extension method is actually invoked fromProgram.cs.
A subtler variant is the unresolvable constructor parameter: a constructor takes a string, int, or an options object that was never configured. The container cannot invent primitives, so the error names the primitive type. Fix it by registering the value (services.AddSingleton(new ConnectionStrings(...))) or binding configuration with services.Configure<OrderOptions>(configuration.GetSection("Orders")) and injecting IOptions<OrderOptions>.
Check 3: Scoped service captured by a singleton
A singleton is created once and lives for the process. If its constructor takes a scoped service (one instance per HTTP request, such as an EF Core DbContext), that scoped instance is captured forever. With scope validation on, you get InvalidOperationException: Cannot consume scoped service 'X' from singleton 'Y'. With validation off (the default outside Development), you get something worse: no error, and every request shares the first request's scoped instance — stale data, disposed-context exceptions, or cross-user leakage.
To find the capture, walk the constructor chain from the singleton named in the exception. Fixes, in order of preference:
- Change the consumer's lifetime to scoped if it is request-bound work.
- If the consumer must stay singleton (a background service, a cache), inject
IServiceScopeFactoryand create a scope per unit of work:
public class ReportCache
{
private readonly IServiceScopeFactory _scopeFactory;
public ReportCache(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;
public async Task RefreshAsync()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// use db within this scope only
}
}Do not fix this by promoting the scoped service itself to singleton. A singleton DbContext, for example, is not thread-safe and will corrupt state under concurrent requests.
Check 4: Circular dependencies
If service A's constructor needs B and B's constructor needs A, the container cannot construct either. Depending on version, you get an exception mentioning a circular dependency or a hang/stack overflow during resolution. The diagnostic is simple: list each service's constructor parameters and look for a cycle. The fix is a design change, not a registration tweak — extract the shared responsibility into a third service both depend on, or break the cycle by having one side depend on an abstraction it receives via method parameter rather than constructor. Injecting IServiceProvider and resolving lazily breaks the cycle mechanically but hides the design problem and defers failures to runtime; treat it as a last resort.
Check 5: Empty IEnumerable<T> and open generics
Injecting IEnumerable<IValidator> never throws — the container happily injects an empty sequence if nothing is registered. If your pipeline silently skips validators or handlers, count the registrations. Each implementation needs its own registration line (services.AddScoped<IValidator, OrderValidator>()); registering the concrete classes alone is not enough. For open generics, confirm the mapping direction: services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)) maps the open interface to the open implementation; swapping the arguments registers nothing useful.
Verify the fix
After each fix, re-run the isolated ServiceProvider reproduction from Check 1 — it should now resolve the root service without exceptions. Then exercise the real endpoint with an integration request and scan the logs for No service for type, scope validation errors, or circular-dependency messages. For lifetime bugs specifically, make two sequential requests that touch the same data and confirm the second request sees fresh, request-correct state.
When to escalate
Escalate beyond configuration-level fixes when: the cycle reveals two services that genuinely own each other's responsibilities (a design review, not a registration edit); you need features the built-in container does not support, such as interception, child containers, or property injection (adopt a third-party container deliberately rather than working around it with service-locator calls); or the failure only appears under load, which suggests a threading issue in a captured dependency rather than a DI problem at all. In every case, the minimal reproduction from Check 1 is the artifact to hand to whoever reviews it — it pins the exception type, the missing or mis-lifetimed type, and the registration set in one file.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.