Dependency injection guidelines
This article provides general guidelines and best practices for implementing dependency injection in .NET applications.
Design services for dependency injection
When designing services for dependency injection:
- Avoid stateful, static classes and members. Avoid creating global state by designing apps to use singleton services instead.
- Avoid direct instantiation of dependent classes within services. Direct instantiation couples the code to a particular implementation.
- Make services small, well-factored, and easily tested.
If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the Single Responsibility Principle (SRP). Attempt to refactor the class by moving some of its responsibilities into new classes.
Disposal of services
The container is responsible for cleanup of types it creates, and calls Dispose on IDisposable instances. Services resolved from the container should never be disposed by the developer. If a type or factory is registered as a singleton, the container disposes the singleton automatically.
In the following example, the services are created by the service container and disposed automatically:
namespace ConsoleDisposable.Example;
public sealed class TransientDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}
The preceding disposable is intended to have a transient lifetime.
namespace ConsoleDisposable.Example;
public sealed class ScopedDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}
The preceding disposable is intended to have a scoped lifetime.
namespace ConsoleDisposable.Example;
public sealed class SingletonDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}
The preceding disposable is intended to have a singleton lifetime.
using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();
using IHost host = builder.Build();
ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();
ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();
await host.RunAsync();
static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
Console.WriteLine($"{scope}...");
using IServiceScope serviceScope = services.CreateScope();
IServiceProvider provider = serviceScope.ServiceProvider;
_ = provider.GetRequiredService<TransientDisposable>();
_ = provider.GetRequiredService<ScopedDisposable>();
_ = provider.GetRequiredService<SingletonDisposable>();
}
The debug console shows the following sample output after running:
Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()
Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()
info: Microsoft.Hosting.Lifetime[0]
Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
SingletonDisposable.Dispose()
Services not created by the service container
Consider the following code:
// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());
In the preceding code:
- The
ExampleService
instance is not created by the service container. - The framework does not dispose of the services automatically.
- The developer is responsible for disposing the services.
IDisposable guidance for transient and shared instances
Transient, limited lifetime
Scenario
The app requires an IDisposable instance with a transient lifetime for either of the following scenarios:
- The instance is resolved in the root scope (root container).
- The instance should be disposed before the scope ends.
Solution
Use the factory pattern to create an instance outside of the parent scope. In this situation, the app would generally have a Create
method that calls the final type's constructor directly. If the final type has other dependencies, the factory can:
- Receive an IServiceProvider in its constructor.
- Use ActivatorUtilities.CreateInstance to instantiate the instance outside of the container, while using the container for its dependencies.
Shared instance, limited lifetime
Scenario
The app requires a shared IDisposable instance across multiple services, but the IDisposable instance should have a limited lifetime.
Solution
Register the instance with a scoped lifetime. Use IServiceScopeFactory.CreateScope to create a new IServiceScope. Use the scope's IServiceProvider to get required services. Dispose the scope when it's no longer needed.
General IDisposable
guidelines
- Don't register IDisposable instances with a transient lifetime. Use the factory pattern instead.
- Don't resolve IDisposable instances with a transient or scoped lifetime in the root scope. The only exception to this is if the app creates/recreates and disposes IServiceProvider, but this isn't an ideal pattern.
- Receiving an IDisposable dependency via DI doesn't require that the receiver implement IDisposable itself. The receiver of the IDisposable dependency shouldn't call Dispose on that dependency.
- Use scopes to control the lifetimes of services. Scopes aren't hierarchical, and there's no special connection among scopes.
For more information on resource cleanup, see Implement a Dispose
method, or Implement a DisposeAsync
method. Additionally, consider the Disposable transient services captured by container scenario as it relates to resource cleanup.
Default service container replacement
The built-in service container is designed to serve the needs of the framework and most consumer apps. We recommend using the built-in container unless you need a specific feature that it doesn't support, such as:
- Property injection
- Injection based on name (.NET 7 and earlier versions only. For more information, see Keyed services.)
- Child containers
- Custom lifetime management
Func<T>
support for lazy initialization- Convention-based registration
The following third-party containers can be used with ASP.NET Core apps:
Thread safety
Create thread-safe singleton services. If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton.
The factory method of a singleton service, such as the second argument to AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), doesn't need to be thread-safe. Like a type (static
) constructor, it's guaranteed to be called only once by a single thread.
Recommendations
async/await
andTask
based service resolution isn't supported. Because C# doesn't support asynchronous constructors, use asynchronous methods after synchronously resolving the service.- Avoid storing data and configuration directly in the service container. For example, a user's shopping cart shouldn't typically be added to the service container. Configuration should use the options pattern. Similarly, avoid "data holder" objects that only exist to allow access to another object. It's better to request the actual item via DI.
- Avoid static access to services. For example, avoid capturing IApplicationBuilder.ApplicationServices as a static field or property for use elsewhere.
- Keep DI factories fast and synchronous.
- Avoid using the service locator pattern. For example, don't invoke GetService to obtain a service instance when you can use DI instead.
- Another service locator variation to avoid is injecting a factory that resolves dependencies at run time. Both of these practices mix Inversion of Control strategies.
- Avoid calls to BuildServiceProvider when configuring services. Calling
BuildServiceProvider
typically happens when the developer wants to resolve a service when registering another service. Instead, use an overload that includes theIServiceProvider
for this reason. - Disposable transient services are captured by the container for disposal. This can turn into a memory leak if resolved from the top-level container.
- Enable scope validation to make sure the app doesn't have singletons that capture scoped services. For more information, see Scope validation.
Like all sets of recommendations, you may encounter situations where ignoring a recommendation is required. Exceptions are rare, mostly special cases within the framework itself.
DI is an alternative to static/global object access patterns. You may not be able to realize the benefits of DI if you mix it with static object access.
Example anti-patterns
In addition to the guidelines in this article, there are several anti-patterns you should avoid. Some of these anti-patterns are learnings from developing the runtimes themselves.
Warning
These are example anti-patterns, do not copy the code, do not use these patterns, and avoid these patterns at all costs.
Disposable transient services captured by container
When you register Transient services that implement IDisposable, by default the DI container will hold onto these references, and not Dispose() of them until the container is disposed when application stops if they were resolved from the container, or until the scope is disposed if they were resolved from a scope. This can turn into a memory leak if resolved from container level.
In the preceding anti-pattern, 1,000 ExampleDisposable
objects are instantiated and rooted. They will not be disposed of until the serviceProvider
instance is disposed.
For more information on debugging memory leaks, see Debug a memory leak in .NET.
Async DI factories can cause deadlocks
The term "DI factories" refers to the overload methods that exist when calling Add{LIFETIME}
. There are overloads accepting a Func<IServiceProvider, T>
where T
is the service being registered, and the parameter is named implementationFactory
. The implementationFactory
can be provided as a lambda expression, local function, or method. If the factory is asynchronous, and you use Task<TResult>.Result, this will cause a deadlock.
In the preceding code, the implementationFactory
is given a lambda expression where the body calls Task<TResult>.Result on a Task<Bar>
returning method. This causes a deadlock. The GetBarAsync
method simply emulates an asynchronous work operation with Task.Delay, and then calls GetRequiredService<T>(IServiceProvider).
For more information on asynchronous guidance, see Asynchronous programming: Important info and advice. For more information debugging deadlocks, see Debug a deadlock in .NET.
When you're running this anti-pattern and the deadlock occurs, you can view the two threads waiting from Visual Studio's Parallel Stacks window. For more information, see View threads and tasks in the Parallel Stacks window.
Captive dependency
The term "captive dependency" was coined by Mark Seemann, and refers to the misconfiguration of service lifetimes, where a longer-lived service holds a shorter-lived service captive.
In the preceding code, Foo
is registered as a singleton and Bar
is scoped - which on the surface seems valid. However, consider the implementation of Foo
.
namespace DependencyInjection.AntiPatterns;
public class Foo(Bar bar)
{
}
The Foo
object requires a Bar
object, and since Foo
is a singleton, and Bar
is scoped - this is a misconfiguration. As is, Foo
would only be instantiated once, and it would hold onto Bar
for its lifetime, which is longer than the intended scoped lifetime of Bar
. You should consider validating scopes, by passing validateScopes: true
to the BuildServiceProvider(IServiceCollection, Boolean). When you validate the scopes, you'd get an InvalidOperationException with a message similar to "Cannot consume scoped service 'Bar' from singleton 'Foo'.".
For more information, see Scope validation.
Scoped service as singleton
When using scoped services, if you're not creating a scope or within an existing scope - the service becomes a singleton.
In the preceding code, Bar
is retrieved within an IServiceScope, which is correct. The anti-pattern is the retrieval of Bar
outside of the scope, and the variable is named avoid
to show which example retrieval is incorrect.