Dependency injection lets a class receive collaborators instead of constructing them internally. Registration also decides how long those collaborators live. A service that is safe within one request may become unsafe when retained by a singleton for the application's entire lifetime. Understanding that ownership boundary is essential when an application uses database contexts, request state or disposable resources.
This guide uses .NET 8 and C# 12 with the built-in Microsoft container. The console project references the installed ASP.NET Core shared framework to access its dependency-injection libraries. You will compare instances within and across scopes, enable validation, and create a deliberate short-lived scope from a longer-lived coordinator.
Lifetimes describe sharing and disposal
A transient service is created for each resolution. A scoped service is shared within one scope and differs between scopes. A singleton is shared by the container for its lifetime. These definitions describe instance identity, not whether the class is automatically thread-safe. A singleton accessed by concurrent requests needs an appropriate state design.
In an ASP.NET Core application, a request normally provides a scope. A plain console application must create meaningful scopes explicitly if it uses scoped services. Resolving a scoped service from the root provider can extend its effective lifetime until root disposal and is usually a design mistake. Keep the root as the composition boundary and resolve scoped work inside a created scope, rather than treating the container as a global service locator available everywhere.
Constructor dependencies reveal the class contract
A constructor parameter makes a required collaborator visible. The container resolves the dependency graph and constructs the object under the registered rules. An interface can make behavior substitutable for tests, but do not introduce an interface merely to obscure a fixed implementation with no meaningful alternative.
Avoid injecting IServiceProvider into every domain class and looking up dependencies ad hoc. That hides what the class needs and moves missing-registration failures deeper into execution. Factories or an explicit scope factory can be appropriate at a composition boundary where dynamic creation is a real responsibility. Keep ordinary services focused on typed collaborators, so their behavior can be tested without constructing a full container.
Captive dependencies violate the intended boundary
A singleton that directly receives a scoped service retains it beyond the scope it was meant to serve. This is a captive dependency. Request-specific state can leak across operations, and an otherwise safe scoped database context can be shared unsafely. Enable scope validation to catch this class of registration error.
A singleton holding a transient also retains that particular instance, even though the registration says transient. The resulting lifetime is determined by the object graph, not the label alone. Review disposable transients and large retained objects carefully. A solution is often to change the service's lifetime or create a scope per unit of work, not merely to disable validation. Validation supports design; it should not be treated as an inconvenience to bypass.
The container owns what it creates
A container disposes applicable services it creates when the relevant scope or provider ends. A consumer should not independently dispose an injected service it merely borrows. When an externally created instance is registered, ownership rules differ; inspect the registration contract instead of assuming all objects are container-created.
Disposal can be asynchronous. If a scope contains services requiring async disposal, use an appropriate asynchronous scope and await its cleanup. Do not keep a scoped reference after disposing its scope, and do not return deferred work that depends on a closed scope. Cancellation does not automatically transfer ownership. The operation still needs to finish or abandon its work under a defined cleanup boundary.
Validate registrations and test behavior
Build-time validation can catch many unresolved dependencies and incompatible graphs, but not every runtime factory path or configuration error. Test the composition root and meaningful scope behavior separately from a service's business logic. Unit tests can construct ordinary services directly with fakes, which keeps failures focused.
Do not call BuildServiceProvider repeatedly during registration to resolve temporary instances. That creates extra containers with separate singleton identities and disposal responsibilities. Register the graph first, then create the intended provider or host. Keep the number of ownership roots clear, use explicit scopes for operations, and review whether shared services hold request-specific mutable state.
Official references
C# Dependency Injection Lifetimes Examples
Each example shows the code, the output it produces and what each part does.
Compare a scoped service across scopes
Use this project file and complete program. No external NuGet package is required because the referenced shared framework supplies the container.
The first scope supplies one shared instance. The second scope supplies another. Retaining first here is only for identity comparison; real code must not continue using a disposed scoped resource after its scope ends.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>using Microsoft.Extensions.DependencyInjection;
var registrations = new ServiceCollection();
registrations.AddScoped<WorkContext>();
using var provider = registrations.BuildServiceProvider(
new ServiceProviderOptions
{
ValidateScopes = true,
ValidateOnBuild = true
});
WorkContext first;
using (var scope = provider.CreateScope())
{
first = scope.ServiceProvider.GetRequiredService<WorkContext>();
var again = scope.ServiceProvider.GetRequiredService<WorkContext>();
Console.WriteLine(ReferenceEquals(first, again));
}
using (var scope = provider.CreateScope())
{
var second = scope.ServiceProvider.GetRequiredService<WorkContext>();
Console.WriteLine(ReferenceEquals(first, second));
}
public sealed class WorkContext { }True
FalseC# Dependency Injection Lifetimes in Real Projects
Worked exercise: a scope for each unit of work
The task is to let a singleton coordinator perform two independent jobs, each using its own scoped disposable worker. Inject the scope factory at this explicit lifetime boundary. The worker prints its use and cleanup so the result demonstrates ownership, not just successful resolution. Use the same project file as above.
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddScoped<JobWorker>();
services.AddSingleton<JobCoordinator>();
using var provider = services.BuildServiceProvider(
new ServiceProviderOptions
{
ValidateScopes = true,
ValidateOnBuild = true
});
var coordinator = provider.GetRequiredService<JobCoordinator>();
coordinator.Run("A");
coordinator.Run("B");
public sealed class JobCoordinator
{
private readonly IServiceScopeFactory scopes;
public JobCoordinator(IServiceScopeFactory scopes) => this.scopes = scopes;
public void Run(string name)
{
using var scope = scopes.CreateScope();
scope.ServiceProvider.GetRequiredService<JobWorker>().Run(name);
}
}
public sealed class JobWorker : IDisposable
{
public void Run(string name) => Console.WriteLine($"Job {name}");
public void Dispose() => Console.WriteLine("Worker disposed");
}Job A
Worker disposed
Job B
Worker disposedThe singleton keeps the factory, not a worker. Each operation owns a fresh scope and ends it before beginning the next. An asynchronous version should await both the work and appropriate scope disposal.
C# Dependency Injection Lifetimes FAQ
Is a singleton automatically thread-safe?
No. The lifetime shares one instance; it does not synchronize that instance's methods or fields. A singleton should generally be stateless or explicitly designed for concurrent access. A mutable request context is usually inappropriate as a singleton. Review the full dependency graph because even a stateless singleton can retain an unsafe collaborator.
Why does a scoped service need a scope in a console app?
A console application does not automatically create one request scope per operation. You must decide the unit of work and create its scope. Resolving from the root can extend a scoped service's effective lifetime, often until application shutdown. Scope validation helps expose that mistake. The scope should surround the actual work, including any asynchronous or deferred operations it owns.
Can a singleton use scoped services at all?
It can coordinate scoped work by creating a scope and resolving the needed service inside that boundary. It must not retain the scoped service after the operation ends. Injecting IServiceScopeFactory is appropriate for that explicit coordinator responsibility. Directly injecting the scoped service into the singleton creates a captive dependency and defeats the intended lifetime.
Who disposes a service returned by the container?
For container-created disposable services, the relevant container scope normally owns cleanup. A consumer receiving the service should treat it as borrowed unless the API says otherwise. Externally created registered instances require separate ownership consideration. Never assume that manually disposing an injected service is harmless; other users in the same scope may still need it.
C# Dependency Injection Lifetimes Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
A singleton coordinator needs one scoped worker per job. Which design fits?
C# Dependency Injection Lifetimes Quiz
2 questions to check what you learned.
Two resolutions of a scoped service occur within one scope. What is expected?
- The same scoped instance.
- Two unrelated instances in every case.
- The application's singleton instance automatically.
- A new scope for every constructor parameter.
A singleton directly retains a request-scoped database context. What is the concern?
- The context becomes transient automatically.
- A captive dependency outlives its intended scope and can be shared unsafely.
- The container copies it for every method call.
- Scope validation automatically turns the retained context into a thread-safe singleton.