Service Lifetime Pitfalls in ASP.NET Core's Built-in DI Container
Misregistering service lifetimes can cause state leaks or startup crashes; this post shows how to verify and avoid common DI pitfalls in ASP.NET Core.
28 Apr 2026, 21:00 UTC

Why the wrong service lifetime crashes your app at startup
When you register a service with the wrong lifetime in ASP.NET Core's built-in dependency injection container the results can range from silent state leaks to an outright InvalidOperationException at startup. A common scenario a scoped service that should live for a single HTTP request is accidentally resolved from a singleton keeping request scoped state alive for the entire application lifetime. The container detects this mismatch and throws an exception but only if you build the service provider and attempt the resolution. Without that check the bug may surface only in production under specific request patterns.
ASP.NET Core defines three primary lifetime options each with distinct implications for state sharing and resource management.
Transient
A transient service is instantiated every time it is requested. This is ideal for stateless helpers lightweight calculations or cheap objects where each call should get a fresh instance. Because a new object is created on every resolution transient services do not share state across requests or injections.
Scoped
A scoped service lives for the duration of a logical scope. In ASP.NET Core web applications that scope is an individual HTTP request. Within a single request all components that depend on the same scoped service receive the identical instance enabling them to share request specific data such as a database context or user context. Across different requests a new instance is created.
Singleton
A singleton is created once and reused for the entire application lifetime. This is appropriate for truly stateless helpers configuration wrappers or cached data that is expensive to create but safe to share globally. If a singleton holds request specific state or depends on a scoped service the container will prevent the registration from completing or throw an exception when the dependency graph is resolved.
Open generic registrations and factory delegates wiring without concrete types
Real world applications often need the same service type across many modules. Open generic registrations let you define a single registration that applies to any closed generic type reducing boilerplate.
For example if you have an IRepository{T} interface and a corresponding Repository{T} implementation you can register them with a single statement:
builder.Services.AddScoped(typeof(IRepository{}), typeof(Repository{}));Where to run this command in the Program.cs file of an ASP.NET Core project typically within the var builder = WebApplication.CreateBuilder(args) block. Required permissions standard developer user account no elevated privileges needed. Meaningful placeholders replace IRepository{} and Repository{} with your actual interface and implementation types. Expected check after running the app and making requests verify the correct generic type is resolved without compile time errors. Relevant risk if the implementation depends on a scoped service and is registered as singleton the container will throw an InvalidOperationException at startup preventing the app from running.
Factory delegates provide another layer of flexibility allowing you to resolve other services at creation time:
builder.Services.AddTransient(provider => new MyService(provider.GetRequiredService{ILogger}));Where to run same Program.cs block. Required permissions developer account. Placeholders replace MyService and ILogger with your types. Expected check the service is created once per resolution (transient) or per scope (scoped) and the logger is correctly injected. Risk if the factory captures a scoped service outside its lifetime the same captive dependency exception will surface.
Captive dependencies when scoped outlives its scope
The most dangerous lifetime mismatch occurs when a singleton depends on a scoped service. Because the singleton persists for the apps lifetime it would inadvertently keep a reference to a scoped instance that should be discarded after each request. This is called a captive dependency and it can cause memory leaks stale data or subtle race conditions.
The ASP.NET Core DI container is deliberately strict: if you attempt to resolve a scoped service from a singleton or from the root IServiceProvider outside of a request scope the container throws an InvalidOperationException with a clear message such as:
System.InvalidOperationException Cannot consume scoped service MyScopedService from singleton MySingletonService.This failure is intentional it surfaces the problem early often at application startup or the first request rather than letting corrupted state propagate silently.
Practical verification step after building the service provider try resolving a known scoped service from the root provider. If no exception is thrown the registration is likely incorrect adjust the lifetime accordingly.
var provider = builder.Services.BuildServiceProvider(); var scoped = provider.GetService{IOperationId}; // throws InvalidOperationExceptionWhere to run in a console or test project that references your ASP.NET Core apps assembly or within a unit test project that has access to the built provider. Required permissions read access to the projects bin directory no special privileges. Meaningful placeholders replace IOperationId with your actual interface type. Expected check the call throws InvalidOperationException with a message about consuming a scoped service from a singleton root provider. If the call succeeds you have inadvertently registered the scoped service with a longer lifetime. Relevant risk running this check in production or without proper safeguards could expose the app to the very state corruption bugs the check is meant to prevent.
Actionable checklist
- Prefer constructor injection over provider.GetService (service locator) to get compile time safety and avoid bypassing lifetime checks.
- Register request specific DbContext HTTP clients with per request policies or any stateful helper as Scoped.
- Register truly stateless helpers caches or configuration wrappers as Singleton.
- Use Transient only for lightweight stateless objects that are cheap to create.
- Add lifetime validation to your CI pipeline build the service provider and assert that resolving any registered scoped service from the root provider throws the expected exception.
- Consider adding the Microsoft.Extensions.DependencyInjection.Analyzers NuGet package to catch common lifetime mismatches at compile time.
By treating service lifetimes as a deliberate engineering decision rather than an afterthought you can avoid captive dependencies reduce memory waste and keep your ASP.NET Core applications robust under varying load patterns.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.