Skip to main content

Command Palette

Search for a command to run...

Cannot Resolve Scoped Service From Root Provider in ASP.NET Core: Root Cause and Fix

Published
โ€ข10 min read
Cannot Resolve Scoped Service From Root Provider in ASP.NET Core: Root Cause and Fix

The InvalidOperationException: Cannot resolve scoped service from root provider error is one of the most common failures .NET teams encounter when moving code from local development into production โ€” or when adding background services to an otherwise well-functioning ASP.NET Core application. It surfaces suddenly, blocks startup entirely, and points to a dependency injection lifetime mismatch that is easy to trigger but takes some understanding to fix correctly. The key insight is that the root service provider โ€” the one that lives for the entire application lifetime โ€” cannot create scoped services because doing so would silently extend their lifetime beyond what they were designed for. For developers who want to go deeper on dependency injection lifetime patterns and see how they play out in a full production codebase, the complete implementation with annotated source code is available on Patreon.

Background jobs and hosted services are the most common trigger for this error, and understanding why they are singletons โ€” and what that means for any service they try to consume โ€” is the first step toward a durable fix. Chapter 12 of the ASP.NET Core Web API: Zero to Production course covers this exact scenario inside a full production codebase, walking through IServiceScopeFactory, the BackgroundService lifetime model, and the Outbox pattern that depends on it โ€” all with source code you can run immediately.

ASP.NET Core Web API: Zero to Production

Why the Root Provider Refuses to Resolve Scoped Services

Every ASP.NET Core application has a root IServiceProvider that is created once when the host starts and is disposed only when the host shuts down. Its lifetime spans the entire application. Scoped services, by contrast, are designed to live for the duration of a single request โ€” or, more precisely, for the duration of a single IServiceScope. They carry assumptions about their own lifetime: an ApplicationDbContext registered as scoped, for example, is expected to be created per request, track changes for that request, and then be disposed cleanly.

When the root provider attempts to resolve a scoped service directly โ€” without creating a scope first โ€” the DI container throws InvalidOperationException: Cannot resolve scoped service 'YourServiceType' from root provider. This is a deliberate safety check. If the root provider were allowed to resolve scoped services, those services would effectively become singletons for the application's lifetime, leading to shared state across requests, undisposed database connections, and subtle data consistency bugs.

In development, this check is enforced by default because ValidateScopes is enabled when ASPNETCORE_ENVIRONMENT is Development. In production, ValidateScopes defaults to false โ€” which means the same code that throws in development might silently create a scoped-service-as-singleton in production, only manifesting as slow memory growth or stale data. This is why fixing the root cause โ€” not just suppressing the validation โ€” matters.

How Does This Actually Happen in Practice?

Hosted Services and BackgroundService

The most frequent trigger is an IHostedService or BackgroundService implementation that tries to inject a scoped service via constructor injection. Both IHostedService and BackgroundService implementations are registered as singletons โ€” AddHostedService<T>() registers them with singleton lifetime by design, because a background service lives and runs for the entire application lifetime.

If ExecuteAsync or StartAsync attempts to call into an ApplicationDbContext, a repository interface, or any other scoped service that was injected through the constructor, the DI container sees the root provider resolving a scoped dependency on behalf of a singleton. The result is the exception โ€” or worse, a silent lifetime promotion if scope validation is off.

The same pattern appears in middleware constructors. Middleware is instantiated once, not per request. Constructor-injected services in middleware are resolved from the root provider. Any scoped service injected there inherits singleton lifetime for the duration of the application.

Singleton Services That Consume Scoped Dependencies

A singleton service that declares a dependency on a scoped service in its constructor triggers the same failure. Because the singleton is resolved once from the root provider, its scoped dependency must also be resolved by the root provider โ€” which the container refuses to do when scope validation is enabled.

This pattern is easy to introduce accidentally when developers change a service's registration from scoped to singleton (for performance or architectural reasons) without auditing its dependency tree.

The Correct Fix: IServiceScopeFactory

The canonical solution is to inject IServiceScopeFactory instead of the scoped service itself. IServiceScopeFactory is registered as a singleton and is safe to resolve from the root provider. Within ExecuteAsync or any method that needs a scoped service, you create a scope explicitly, resolve the service from that scope's provider, use it, and dispose the scope when done.

public class DataSyncBackgroundService(IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = scopeFactory.CreateScope();
            var repo = scope.ServiceProvider.GetRequiredService<IDataSyncRepository>();
            await repo.SyncPendingRecordsAsync(stoppingToken);

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

The using ensures the scope โ€” and every service resolved from it โ€” is disposed at the end of each iteration. This is the production-correct pattern. The DbContext tied to the repository lives for exactly one sync operation, then is released cleanly.

For middleware, the fix is different: instead of injecting scoped services via the constructor, inject them through the Invoke or InvokeAsync method parameters. ASP.NET Core resolves middleware method parameters per request, so services injected there are correctly resolved from the per-request scope.

public class AuditMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context, IAuditService auditService)
    {
        await auditService.LogRequestAsync(context);
        await next(context);
    }
}

IAuditService here is resolved from the request scope, not the root provider, even though the middleware itself is a singleton.

When ValidateScopes Is the Messenger, Not the Problem

A common reaction to this error is to disable scope validation:

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = false; // โŒ This hides the bug, it does not fix it
});

This makes the exception disappear โ€” but the underlying lifetime mismatch remains. Scoped services resolved from the root provider will accumulate state, hold open database connections, and leak memory. The right response to this exception is to treat it as a useful signal and fix the registration or resolution pattern, not to silence the validator.

A helpful addition during development is ValidateOnBuild, which catches lifetime mismatches at startup rather than at runtime:

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = builder.Environment.IsDevelopment();
    options.ValidateOnBuild = true; // โœ… Catches issues at startup in all environments
});

ValidateOnBuild = true is safe to run in production โ€” it adds a small startup cost but eliminates an entire class of DI misconfiguration from reaching live requests.

Auditing a Production System for This Problem

If ValidateScopes was disabled in production and the application has been running for some time, the fix is not just to re-enable validation. The service dependency tree needs an audit:

  1. Identify all singleton registrations (including IHostedService, BackgroundService, middleware, and any manually registered singletons).

  2. For each singleton, inspect its constructor dependencies and their registered lifetimes.

  3. Any scoped or transient dependency of a singleton is a candidate for the IServiceScopeFactory pattern.

  4. Re-enable scope validation in staging first to surface issues without risking production.

The ASP.NET Core DI Lifetimes decision guide on this blog covers the broader rules for choosing lifetimes correctly. If your team is working with hosted services and background job patterns specifically, the Background Services in .NET 10 enterprise guide is the right companion.

Prevention Going Forward

Register services with the correct lifetime from the start. Before registering any service as a singleton, ask whether any of its dependencies are scoped or transient. If they are, either redesign the dependency graph or plan to use IServiceScopeFactory from the beginning.

Enable ValidateOnBuild in all environments. The startup cost is negligible and the protection is significant.

Use scoped services through scoped lifetimes. Background services should always create a new scope per unit of work, not hold one open for the entire application lifetime. A scope per iteration of the background loop is the correct model.

Test background services in integration tests using real DI containers. WebApplicationFactory<TProgram> creates a real host with a real DI container. Background service DI mismatches show up immediately when the test host starts.

โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!

FAQ

What does "Cannot resolve scoped service from root provider" mean?

It means the ASP.NET Core DI container detected an attempt to resolve a service with scoped lifetime directly from the root IServiceProvider, which lives for the entire application. Scoped services are designed for per-request lifetimes. Allowing them to be resolved from the root would turn them into effective singletons, causing shared state, undisposed resources, and data integrity problems. The exception is a protective guard against this class of bug.

Why does this error only appear in development and not in production?

By default, ASP.NET Core enables ValidateScopes when the environment is Development and disables it in Production. In production, resolving a scoped service from the root provider does not throw โ€” instead, the service is silently promoted to singleton lifetime. This can manifest as slow memory growth, database connection exhaustion, or stale data. The fix is to resolve the misconfiguration rather than rely on the absence of the exception.

How do I use a scoped service inside a BackgroundService?

Inject IServiceScopeFactory via the BackgroundService constructor (it is a singleton and safe to inject). Inside ExecuteAsync, call scopeFactory.CreateScope() to create a new scope, resolve your scoped service from scope.ServiceProvider, perform the work, and dispose the scope with a using block. This gives each background job iteration its own clean DI scope, equivalent to a per-request scope in a normal API endpoint.

Can I use scoped services inside ASP.NET Core middleware?

Yes, but not via constructor injection. Middleware classes are singletons โ€” constructor-injected dependencies are resolved once from the root provider. To use scoped services in middleware, add them as parameters to the Invoke or InvokeAsync method. ASP.NET Core resolves method-injected dependencies per request from the correct request scope.

What is ValidateOnBuild and should I enable it in production?

ValidateOnBuild = true instructs the DI container to validate the entire service registration graph when the application host is built โ€” at startup, before any requests are served. It catches lifetime mismatches and missing registrations early. It adds a small startup overhead but zero runtime overhead for normal request processing. Enabling it in production is safe and is recommended as a defence-in-depth measure against DI misconfiguration.

What is the difference between IServiceScopeFactory and IServiceProvider for this problem?

IServiceScopeFactory is registered as a singleton and is designed exactly for the use case of creating scopes on demand from long-lived services. IServiceProvider in a background service or singleton is the root provider โ€” calling GetService on it directly is the source of the problem. The IServiceScopeFactory.CreateScope() method returns an IServiceScope whose ServiceProvider is a properly bounded child provider, not the root. Always use IServiceScopeFactory when a singleton needs to resolve scoped dependencies.

Does this problem occur with IHostedService as well as BackgroundService?

Yes. IHostedService implementations registered via AddHostedService<T>() are singletons regardless of whether they extend BackgroundService or implement the interface directly. The same restriction applies: any scoped dependency must be obtained through a freshly created IServiceScope, not injected into the constructor.

More from this blog

C

Coding Droplets

198 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.