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.

di-workflow #spacing: 10 #gravity: .8 #arrowSize: 4 #background: transparent #fill: #f4f6fb #title: di-workflow [<actor>] -[<label>Create]-> [IServiceCollection] -[<frame>Register services| [Singleton ] [Scoped] [Transient] ] -[<label>Call]-> [BuildServiceProvider()] -[<label>Creates]-> [IServiceProvider] [IServiceProvider] -> [Resolve] Create IServiceCollection Register services Singleton Scoped Transient Call BuildServiceProvider() Creates IServiceProvider Resolve

Dependency Injection Workflow

Service lifetime

di-lifetime #background: transparent #fill: #f4f6fb #title: di-lifetime [Singleton] <- [Scoped] <:- [Transient] [Singleton|One instance for the container lifetime, Created on first use] [Transient|New instance on every resolution] [Scoped|One instance per scope] [<note>Longest lifetime Shared, thread-safe, expensive-to-create services] -- [Singleton] [<note>Services that should share state during a request] -- [Transient] [<note>Lightweight, stateless services] -- [Scoped] Singleton One instance for the container lifetime, Created on first use Scoped One instance per scope Transient New instance on every resolution Longest lifetime Shared, thread-safe, expensive-to-create services Services that should share state during a request Lightweight, stateless services

Service lifetimes

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 IDisposable instances 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 IDisposable instances 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 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.

Service Registration

di-registration #direction: right #spacing: 10 #gravity: .8 #arrowSize: 4 #background: transparent #fill: #f4f6fb #title: di-registration [IServiceCollection| Specifies the contract for a collection of service descriptors.] [IServiceCollection|Default implementation of IServiceCollection.] [ServiceCollection| Default implementation of IServiceCollection.] [ServiceDescriptor| Describes a service with its service type, implementation, and lifetime.] [IServiceCollection] <:-- [ServiceCollection] [ICollection<ServiceDescriptor>] <:-- [IServiceCollection] [IEnumerable<ServiceDescriptor>] <:-- [IServiceCollection] [IList<ServiceDescriptor>] <:-- [IServiceCollection] IServiceCollection Specifies the contract for a collection of service descriptors. ServiceCollection Default implementation of IServiceCollection. ServiceDescriptor Describes a service with its service type, implementation, and lifetime. ICollection<ServiceDescriptor> IEnumerable<ServiceDescriptor> IList<ServiceDescriptor>

Dependency Registratgion related types

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

di-registration #direction: right #spacing: 10 #gravity: .8 #arrowSize: 4 #background: transparent #fill: #f4f6fb #title: di-registration [IServiceCollection| Specifies the contract for a collection of service descriptors.] [IServiceCollection|Default implementation of IServiceCollection.] [ServiceCollection| Default implementation of IServiceCollection.] [ServiceDescriptor| Describes a service with its service type, implementation, and lifetime.] [IServiceCollection] <:-- [ServiceCollection] [ICollection<ServiceDescriptor>] <:-- [IServiceCollection] [IEnumerable<ServiceDescriptor>] <:-- [IServiceCollection] [IList<ServiceDescriptor>] <:-- [IServiceCollection] IServiceCollection Specifies the contract for a collection of service descriptors. ServiceCollection Default implementation of IServiceCollection. ServiceDescriptor Describes a service with its service type, implementation, and lifetime. ICollection<ServiceDescriptor> IEnumerable<ServiceDescriptor> IList<ServiceDescriptor>

Dependency Resolve related types

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 returns null if the service type is not registered.

    IMyService? service = serviceProvider.GetService<IMyService>();
    
  • Use GetRequiredService<TService>() when the service must be registered. It throws an InvalidOperationException if the service cannot be resolved.

    IMyService service = serviceProvider.GetRequiredService<IMyService>();
    
  • Keyed services are resolved by supplying the same key used during registration. GetKeyedService<TService>() returns null when no matching registration exists.

    ICache? cache = serviceProvider.GetKeyedService<ICache>(SmallCache);
    
  • Use GetRequiredKeyedService<TService>() when the keyed service must exist. It throws an InvalidOperationException if 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");
}