LEARN · DEBUGGING GUIDE

ASP.NET Core Captive Dependency Scope Error: Diagnosis and Fixes

Captive dependencies in ASP.NET Core DI cause memory leaks, stale state, and hard-to-debug failures. Here's how to spot them, fix them, and never write them again.

AdvancedDotnet7 min read

What this usually means

The root cause is always the same: a service registered as scoped (or transient) is injected into a singleton consumer. The DI container captures the scoped instance at singleton creation time and holds it forever. In ASP.NET Core, scoped services are tied to the HTTP request; when a singleton keeps a reference, that scoped instance lives for the application's lifetime. This breaks the intended disposal and state isolation. The most common culprit is injecting a scoped DbContext (or any scoped service) into a singleton filter, middleware, or service that is registered as Singleton. The error surfaces when the container attempts to resolve that scoped instance from the root provider—something that is explicitly forbidden because scoped instances need a scope.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 11. Enable ValidateScopes in development: call `builder.Host.UseDefaultServiceProvider(options => options.ValidateScopes = true);` in Program.cs.
  • 22. Check the stack trace for the exact error message: 'Cannot resolve scoped service ... from root provider'.
  • 33. Look for singleton registrations that take scoped dependencies in their constructor—scan all AddSingleton() calls.
  • 44. Review custom middleware, action filters, and IHostedService implementations: they often capture scoped services.
  • 55. Run the app with logging verbosity: set 'Logging:LogLevel:Microsoft.Extensions.DependencyInjection' to Debug.
  • 66. Use a static analysis tool like 'Microsoft.Extensions.DependencyInjection.Analyzers' NuGet package.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchProgram.cs or Startup.cs: all service registration calls, especially AddSingleton<>()
  • searchCustom middleware InvokeAsync/Invoke method: constructor-injected services vs method-injected services
  • searchAction filters: IFilterMetadata implementations that inject scoped services in constructor
  • searchIHostedService implementations: BackgroundService or IHostedService that inject scoped services
  • searchAll static or singleton classes that manually new up a scope (e.g., IServiceScopeFactory usage)
  • searchGlobal.asax or module registrations in legacy mixed-code apps
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningInjecting scoped DbContext into a singleton filter or middleware constructor
  • warningRegistering a service as Singleton when it has a scoped dependency (the container itself warns if ValidateScopes is on)
  • warningCapturing scoped service in a static field or a long-lived cache
  • warningUsing IServiceProvider directly (service locator) and resolving scoped service from root provider
  • warningMissing IServiceScopeFactory usage in IHostedService – resolving scoped service per iteration incorrectly
  • warningThird-party library that exposes a singleton interface but internally depends on scoped services
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildChange the consumer's lifetime to Scoped if it's safe (e.g., middleware can be scoped if it doesn't hold state)
  • buildUse IServiceScopeFactory in singletons: create a new scope per operation and resolve scoped services inside that scope
  • buildMove scoped dependencies from constructor to method parameters: in middleware, use InvokeAsync(IScopedService svc) instead of constructor injection
  • buildFor action filters, inject scoped services via the filter's OnActionExecuting method using context.HttpContext.RequestServices
  • buildRegister the problematic service as Scoped everywhere and adjust consumers accordingly
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedSet ValidateScopes = true and ValidateOnBuild = true; the app should throw at startup if any captive dependencies exist
  • verifiedAfter fix, run a load test with steady memory monitoring—no growth over time
  • verifiedCheck that DbContext instances are disposed after each request (e.g., via IDisposable logging or tools like dotnet-counters)
  • verifiedAdd a test that creates a singleton and resolves a scoped service from a child scope—it should get a fresh instance
  • verifiedVerify that background services using IServiceScopeFactory create and dispose scopes correctly per iteration
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't blindly change all singletons to scoped—it may break caching or state expectations
  • warningDon't ignore ValidateScopes warnings in development—they indicate real problems
  • warningDon't use service locator (IServiceProvider.GetService) as a workaround—it hides the real issue
  • warningDon't manually dispose the scoped service in a singleton—you'll get ObjectDisposedException on next use
  • warningDon't forget to dispose the IServiceScope after use in background services—that's a resource leak
( 07 )War story

The Stale DbContext That Caused Ghost Orders

Backend EngineerASP.NET Core 6, Entity Framework Core 6, SQL Server, Azure App Service

Timeline

  1. 09:15User reports seeing another user's order in the 'My Orders' page
  2. 09:20I check logs: no 500 errors, but some DbContext queries return stale data
  3. 09:35I reproduce locally: after first request, subsequent requests show same data even after DB update
  4. 09:50I notice a custom middleware 'TenantResolutionMiddleware' registered as Singleton that injects ITenantContext (scoped)
  5. 10:00I add ValidateScopes = true and app blows up on startup: 'Cannot resolve scoped service from root provider'
  6. 10:10I realize the middleware's constructor captures ITenantContext once—the same tenant context is reused for all requests
  7. 10:20Fix: move ITenantContext from constructor to InvokeAsync parameter (method injection)
  8. 10:25Deploy fix; stale data gone. Memory graph flattens.

We had a multi-tenant e-commerce app on ASP.NET Core 6. A custom middleware resolved the tenant from the subdomain and set it in a scoped ITenantContext. That context was then used by scoped DbContexts via EF Core's query filters. It worked fine in dev, but production started showing cross-tenant data leaks after a deploy.

I initially thought it was a caching issue in the repository layer, but after adding extensive logging, I saw the same TenantId being used across requests. The middleware was registered as a singleton—because it didn't hold state—but it injected the scoped ITenantContext in its constructor. The container resolved it once and held it forever. Every request after the first used the same tenant.

The ValidateScopes flag caught it immediately: the app threw at startup. I switched the ITenantContext dependency to method injection in the middleware's InvokeAsync, and the container resolved a fresh scoped instance per request. No more ghost orders. The lesson: always enable ValidateScopes in development, and never inject scoped services into singleton constructors.

Root cause

Singleton middleware captures scoped ITenantContext in constructor, causing same tenant context across all requests.

The fix

Changed ITenantContext injection from constructor to InvokeAsync method parameter.

The lesson

Enable ValidateScopes=true in development. Use method injection for scoped services in middleware. Treat any singleton that touches scoped data as suspect.

( 08 )How ASP.NET Core DI Validates Scopes

When ValidateScopes is enabled, the container tracks the scope of each registration. Scoped services are tied to a specific IServiceScope. The root provider (the one without a scope) is not allowed to resolve scoped services because they would have no scope to belong to—they'd become singletons accidentally. The container throws an InvalidOperationException when it detects a resolution of a scoped service from the root provider, either directly or through a singleton's dependencies.

The validation happens at resolution time, not registration time, unless ValidateOnBuild is also enabled. For example, registering a singleton that depends on a scoped service will not throw at registration, but the first time the singleton is resolved, it tries to resolve the scoped dependency from the root provider and throws. Setting both flags to true catches these issues at app startup, preventing runtime surprises.

( 09 )Pattern: Method Injection in Middleware

ASP.NET Core middleware has a unique feature: the InvokeAsync (or Invoke) method can accept additional parameters that are resolved from the request's service scope. This is specifically designed to allow middleware to be registered as singletons while still consuming scoped services. The parameters are populated per-request from the current scope.

Example: public async Task InvokeAsync(HttpContext context, ITenantContext tenantContext). The middleware class itself has no constructor dependencies. This avoids captive dependencies entirely. Note that this only works for middleware—not for other singleton consumers like IHostedService or filters. For those, you must use IServiceScopeFactory.

( 10 )Using IServiceScopeFactory in Singletons

When you have a singleton that must perform work with scoped services (e.g., a background service that processes queue items), inject IServiceScopeFactory into the singleton's constructor. Then, for each unit of work, create a new scope with using var scope = _scopeFactory.CreateScope(); and resolve scoped services from scope.ServiceProvider.

This pattern ensures that scoped services are created and disposed correctly per operation. A common mistake is to reuse the same scope or to forget to dispose it, leading to memory leaks. Always wrap CreateScope in a using block. Also, do not store the scope instance—create and dispose per logical operation.

( 11 )Diagnosing Captive Dependencies in Third-Party Libraries

Sometimes the captive dependency is not in your code but in a third-party registration. For example, a library might register its own service as Singleton but internally depend on a scoped service like IHttpContextAccessor. When you call into that library, you hit the error. To diagnose: enable ValidateScopes and note the stack trace. The exception message shows which service cannot be resolved and the consumer that requested it.

If the library is open source, check its DI registration extensions. Often, the fix is to register the library's services as Scoped instead of Singleton, or to wrap the library call in a scope. If you cannot change the library code, you might need to override the registration in your composition root: services.AddScoped<ILibraryService, LibraryService>(...).

( 12 )Testing for Captive Dependencies

You can write a unit test that verifies your composition root does not have captive dependencies. Build the service provider with ValidateScopes and ValidateOnBuild enabled, then resolve critical singletons. If the test passes without exception, the registration is safe.

Example: var services = new ServiceCollection(); ... var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true }); provider.GetRequiredService<IMySingleton>();. This will throw if that singleton or any of its transitive dependencies are scoped. Automate this test in your CI pipeline to catch regressions.

Frequently asked questions

What exactly is a 'captive dependency' in ASP.NET Core DI?

A captive dependency occurs when a service with a shorter lifetime (scoped or transient) is injected into a service with a longer lifetime (singleton). The singleton holds a reference to the scoped instance for its entire lifetime, 'capturing' it. This causes the scoped service to behave like a singleton, breaking expected disposal and state isolation.

Why does ASP.NET Core throw 'Cannot resolve scoped service from root provider'?

The root provider is the container's top-level provider without any scope. Scoped services are not supposed to be resolved from it because they need a scope to be created and disposed. When a singleton (which is resolved from the root provider) depends on a scoped service, the container attempts to resolve that scoped service from the root provider, which is forbidden. This throws the error.

Can I use IServiceProvider.GetService to work around captive dependency errors?

No. Using IServiceProvider directly (service locator pattern) to resolve scoped services from the root provider will still throw if ValidateScopes is enabled. Even if you disable validation, you'll get a captive dependency—the scoped service will be alive forever, causing memory leaks and stale state. Always use IServiceScopeFactory to create a scope for scoped resolutions in singletons.

How do I fix a captive dependency in an IHostedService?

Inject IServiceScopeFactory into your IHostedService (or BackgroundService). In the ExecuteAsync loop, create a new scope per iteration: using var scope = _scopeFactory.CreateScope(); var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();. Then work with scopedService. Ensure the scope is disposed after use (the using block does this).

Does ValidateScopes affect performance in production?

ValidateScopes adds a small overhead at service resolution time because it tracks the scope of each resolved service. In production, it's typically disabled (default). However, it's highly recommended to enable it in development to catch captive dependencies early. You can also enable it in staging environments for integration tests. The performance impact is negligible for most apps.