This article provides general guidelines and best practices for implementing dependency injection in .NET applications.
Design services for dependency injectionWhen designing services for dependency injection:
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 servicesThe 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:
ExampleService
instance is not created by the service container.Scenario
The app requires an IDisposable instance with a transient lifetime for either of the following scenarios:
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:
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.
GeneralIDisposable
guidelines
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.
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:
Func<T>
support for lazy initializationThe following third-party containers can be used with ASP.NET Core apps:
Thread safetyCreate 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.
async/await
and Task
based service resolution isn't supported. Because C# doesn't support asynchronous constructors, use asynchronous methods after synchronously resolving the service.BuildServiceProvider
typically happens when the developer wants to resolve a service when registering another service. Instead, use an overload that includes the IServiceProvider
for this reason.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-patternsIn 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 containerWhen 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 deadlocksThe 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 dependencyThe 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 singletonWhen 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.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4