Dependency Injection
Dependency Injection (DI) is a design pattern where a class receives its dependencies (for example, services it needs) from the outside instead of creating them itself, which improves testability, flexibility, and separation of concerns.
In modern .NET, Microsoft.Extensions.DependencyInjection is the go-to standard container because it is built into ASP.NET Core. The DI stack is intentionally split into an abstractions package: Microsoft.Extensions.DependencyInjection.Abstractions and an implementation package: Microsoft.Extensions.DependencyInjection.
Service lifetime
Sope: In ASP.NET one HTTP request one scope.
Dependency injection guidelines
- 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.
- 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. The container disposes services automatically based on their lifetime:
- Transient and scoped services are disposed at the end of the scope in which they were resolved. In apps that process requests, this is typically at the end of the request.
- Singleton services are disposed when the service container is disposed, usually at application shutdown.
- Don't register
IDisposableinstances with a transient lifetime. Use the factory pattern instead so the solved service can be manually disposed when it's no longer in use. - Don't resolve
IDisposableinstances with a transient or scoped lifetime in the root scope. The only exception to this is if the app creates or recreates and disposes IServiceProvider, but this isn't an ideal pattern. - Receiving an
IDisposabledependency via DI doesn't require that the receiver implementIDisposableitself. The receiver of theIDisposabledependency shouldn't callDispose()on that dependency. - Use scopes to control the lifetimes of services. Scopes aren't hierarchical, and there's no special connection among scopes.
Service Registration
Rule of thumb: Avoid Captive dependencies! A captive dependency happens when a service with a longer lifetime captures a service with a shorter lifetime.
public class MyService
{
private readonly MyDbContext _dbContext;
public MyService(MyDbContext dbContext)
{
_dbContext = dbContext;
}
}
services.AddScoped<MyDbContext>();
//captive dependency, since MyService is singleton
services.AddSingleton<MyService>();
In this example this is bad, because an instance of MyDbContext outlives the scope and will not be disposed.
| Depender | Dependency | State |
|---|---|---|
| Sigleton | Sigleton | Okay |
| Sigleton | Scoped | Captive dependency |
| Sigleton | Transient | Captive dependency |
| Scoped | Sigleton | Okay |
| Scoped | Scoped | Okay |
| Scoped | Transient | Captive dependency |
| Transient | Singleton | Okay |
| Transient | Scoped | Okay |
| Transient | Transient | Okay |
var collection = new SServiceCollection();
//Singleton registration methods
collection.AddSingleton(Type serviceType);
collection.AddSingleton(Type serviceType, object implementation);
collection.AddSingleton(Type serviceType, Type implementationType);
collection.AddSingleton(Type serviceType, Func<IServiceProvider,object> implementationFactory);
collection.AddSingleton<TService>();
collection.AddSingleton<TService>(Func<IServiceProvider,TService> implementationFactory);
collection.AddSingleton<TService>(TService implementation);
collection.AddSingleton<TService,TImplementation>()
collection.AddSingleton<TService,TImplementation>(Func<IServiceProvider,TImplementation> implementationFactory);
//Scoped registrations
collection.AddScoped(Type serviceType);
collection.AddScoped(Type serviceType, Type implementationType);
collection.AddScoped(Type serviceType, Func<IServiceProvider,object> implementationFactory);
collection.AddScoped<TService>();
collection.AddScoped<TService>(Func<IServiceProvider,TService> implementationFactory);
collection.AddScoped<TService,TImplementation>();
collection.AddScoped<TService,TImplementation>(Func<IServiceProvider,TImplementation> implementationFactory);
//Transient registrations
collection.AddTransient(Type serviceType);
collection.AddTransient(Type serviceType, Type implementationType);
collection.AddTransient(Type serviceType, Func<IServiceProvider,object> implementationFactory);
collection.AddTransient<TService>();
collection.AddTransient<TService>(Func<IServiceProvider,TService> implementationFactory);
collection.AddTransient<TService,TImplementation>();
collection.AddTransient<TService,TImplementation>(Func<IServiceProvider,TImplementation> implementationFactory);
keyed services
Keyed services allow multiple implementations of the same service type to be registered under unique keys. They are available in .NET 8 and later. Keys are object values, but constants or enums help avoid mismatches between registration and resolution.
const string SmallCache = "small";
const string LargeCache = "large";
services.AddKeyedSingleton<ICache, SmallCache>(SmallCache);
services.AddKeyedScoped<ICache, LargeCache>(LargeCache);
services.AddKeyedTransient<IFormatter, JsonFormatter>("json");
Use GetKeyedService<TService>(key) when the registration is optional, or GetRequiredKeyedService<TService>(key) when a missing registration should throw an exception.
Resolving services
After all services have been registered, an IServiceProvider must be built from the IServiceCollection. It is recommended to enable ValidateOnBuild and ValidateScopes so invalid registrations and lifetime issues are detected when the provider is created, rather than later when a service is resolved:
IServiceProvider serviceProvider = collection.BuildServiceProvider(new ServiceProviderOptions
{
// Validate service registrations when building the service provider.
// Ensures that all dependencies can be resolved.
ValidateOnBuild = true,
// Validate scopes when building the service provider.
// This is useful for detecting captive dependencies.
ValidateScopes = true
});
Use
GetService<TService>()when a service is optional. It returnsnullif the service type is not registered.IMyService? service = serviceProvider.GetService<IMyService>();Use
GetRequiredService<TService>()when the service must be registered. It throws anInvalidOperationExceptionif the service cannot be resolved.IMyService service = serviceProvider.GetRequiredService<IMyService>();Keyed services are resolved by supplying the same key used during registration.
GetKeyedService<TService>()returnsnullwhen no matching registration exists.ICache? cache = serviceProvider.GetKeyedService<ICache>(SmallCache);Use
GetRequiredKeyedService<TService>()when the keyed service must exist. It throws anInvalidOperationExceptionif no service is registered for the specified type and key.ICache cache = serviceProvider.GetRequiredKeyedService<ICache>(SmallCache);
Multiple implementations
Multiple implementations can be registered for the same service type. When resolving a single service with GetService<TService>() or GetRequiredService<TService>(), the built-in container returns the last registered implementation.
services.AddTransient<IMessageWriter, ConsoleMessageWriter>();
services.AddTransient<IMessageWriter, FileMessageWriter>();
// Resolves FileMessageWriter because it was registered last.
IMessageWriter writer = serviceProvider.GetRequiredService<IMessageWriter>();
Use GetServices<TService>() to resolve all registered implementations. They are returned in registration order; if none are registered, an empty collection is returned.
IEnumerable<IMessageWriter> writers =
serviceProvider.GetServices<IMessageWriter>();
// ConsoleMessageWriter, then FileMessageWriter
foreach (IMessageWriter writer in writers)
{
writer.Write("Hello");
}