Dependency injection in .NET

.NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. Dependency injection in .NET is a first-class citizen, along with configuration, logging, and the options pattern.

A dependency is an object that another object depends on. Examine the following MessageWriter class with a Write method that other classes depend on:

public class MessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

A class can create an instance of the MessageWriter class to make use of its Write method. In the following example, the MessageWriter class is a dependency of the Worker class:

public class Worker : BackgroundService
{
    private readonly MessageWriter _messageWriter = new MessageWriter();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1000, stoppingToken);
        }
    }
}

The class creates and directly depends on the MessageWriter class. Hard-coded dependencies, such as in the previous example, are problematic and should be avoided for the following reasons:

  • To replace MessageWriter with a different implementation, the MessageService class must be modified.
  • If MessageWriter has dependencies, they must also be configured by the MessageService class. In a large project with multiple classes depending on MessageWriter, the configuration code becomes scattered across the app.
  • This implementation is difficult to unit test. The app should use a mock or stub MessageWriter class, which isn't possible with this approach.

Dependency injection addresses these problems through:

  • The use of an interface or base class to abstract the dependency implementation.
  • Registration of the dependency in a service container. .NET provides a built-in service container, IServiceProvider. Services are typically registered at the app's start-up, and appended to an IServiceCollection. Once all services are added, you use BuildServiceProvider to create the service container.
  • Injection of the service into the constructor of the class where it's used. The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it's no longer needed.

As an example, the IMessageWriter interface defines the Write method:

namespace DependencyInjection.Example
{
    public interface IMessageWriter
    {
        void Write(string message);
    }
}

This interface is implemented by a concrete type, MessageWriter:

using System;

namespace DependencyInjection.Example
{
    public class MessageWriter : IMessageWriter
    {
        public void Write(string message)
        {
            Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
        }
    }
}

The sample code registers the IMessageWriter service with the concrete type MessageWriter. The AddScoped method registers the service with a scoped lifetime, the lifetime of a single request. Service lifetimes are described later in this topic.

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    class Program
    {
        static Task Main(string[] args) =>
            CreateHostBuilder(args).Build().RunAsync();

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddHostedService<Worker>()
                            .AddScoped<IMessageWriter, MessageWriter>());
    }
}

In the sample app, the IMessageWriter service is requested and used to call the Write method:

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    public class Worker : BackgroundService
    {
        private readonly IMessageWriter _messageWriter;

        public Worker(IMessageWriter messageWriter) =>
            _messageWriter = messageWriter;

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
                await Task.Delay(1000, stoppingToken);
            }
        }
    }
}

By using the DI pattern, the worker service:

  • Doesn't use the concrete type MessageWriter, only the IMessageWriter interface that implements it. That makes it easy to change the implementation that the controller uses without modifying the controller.
  • Doesn't create an instance of MessageWriter, it's created by the DI container.

The implementation of the IMessageWriter interface can be improved by using the built-in logging API:

using Microsoft.Extensions.Logging;

namespace DependencyInjection.Example
{
    public class LoggingMessageWriter : IMessageWriter
    {
        private readonly ILogger<LoggingMessageWriter> _logger;

        public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
            _logger = logger;

        public void Write(string message) => 
            _logger.LogInformation(message);
    }
}

The updated ConfigureServices method registers the new IMessageWriter implementation:

static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((_, services) =>
            services.AddHostedService<Worker>()
                    .AddScoped<IMessageWriter, LoggingMessageWriter>());

LoggingMessageWriter depends on ILogger<TCategoryName>, which it requests in the constructor. ILogger<TCategoryName> is a framework-provided service.

It's not unusual to use dependency injection in a chained fashion. Each requested dependency in turn requests its own dependencies. The container resolves the dependencies in the graph and returns the fully resolved service. The collective set of dependencies that must be resolved is typically referred to as a dependency tree, dependency graph, or object graph.

The container resolves ILogger<TCategoryName> by taking advantage of (generic) open types, eliminating the need to register every (generic) constructed type.

In dependency injection terminology, a service:

  • Is typically an object that provides a service to other objects, such as the IMessageWriter service.
  • Is not related to a web service, although the service may use a web service.

The framework provides a robust logging system. The IMessageWriter implementations shown in the preceding examples were written to demonstrate basic DI, not to implement logging. Most apps shouldn't need to write loggers. The following code demonstrates using the default logging, which only requires the Worker to be registered in ConfigureServices as a hosted service AddHostedService:

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger) =>
        _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Using the preceding code, there is no need to update ConfigureServices, because logging is provided by the framework.

Register groups of services with extension methods

Microsoft Extensions uses a convention for registering a group of related services. The convention is to use a single Add{GROUP_NAME} extension method to register all of the services required by a framework feature. For example, the AddOptions extension method registers all of the services required for using options.

Framework-provided services

The ConfigureServices method registers services that the app uses, including platform features. Initially, the IServiceCollection provided to ConfigureServices has services defined by the framework depending on how the host was configured. For apps based on the .NET templates, the framework registers hundreds of services.

The following table lists a small sample of these framework-registered services:

Service Type Lifetime
IHostApplicationLifetime Singleton
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transient
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticListener Singleton
System.Diagnostics.DiagnosticSource Singleton

Service lifetimes

Services can be registered with one of the following lifetimes:

  • Transient
  • Scoped
  • Singleton

The following sections describe each of the preceding lifetimes. Choose an appropriate lifetime for each registered service.

Transient

Transient lifetime services are created each time they're requested from the service container. This lifetime works best for lightweight, stateless services. Register transient services with AddTransient.

In apps that process requests, transient services are disposed at the end of the request.

Scoped

For web applications a scoped lifetime indicates that services are created once per client request (connection). Register scoped services with AddScoped.

In apps that process requests, scoped services are disposed at the end of the request.

When using Entity Framework Core, the AddDbContext extension method registers DbContext types with a scoped lifetime by default.

Note

Do not resolve a scoped service from a singleton. It may cause the service to have incorrect state when processing subsequent requests. It's fine to:

  • Resolve a singleton service from a scoped or transient service.
  • Resolve a scoped service from another scoped or transient service.

By default, in the development environment, resolving a service from another service with a longer lifetime throws an exception. For more information, see Scope validation.

Singleton

Singleton lifetime services are created either:

  • The first time they're requested.
  • By the developer, when providing an implementation instance directly to the container. This approach is rarely needed.

Every subsequent request of the service implementation from the dependency injection container uses the same instance. If the app requires singleton behavior, allow the service container to manage the service's lifetime. Don't implement the singleton design pattern and provide code to dispose of the singleton. Services should never be disposed by code that resolved the service from the container. If a type or factory is registered as a singleton, the container disposes the singleton automatically.

Register singleton services with AddSingleton. Singleton services must be thread safe and are often used in stateless services.

In apps that process requests, singleton services are disposed when the ServiceProvider is disposed on application shutdown. Because memory is not released until the app is shut down, consider memory use with a singleton service.

Warning

Do not resolve a scoped service from a singleton. It may cause the service to have incorrect state when processing subsequent requests. It's fine to resolve a singleton service from a scoped or transient service.

Service registration methods

The framework provides service registration extension methods that are useful in specific scenarios:

Method Automatic
object
disposal
Multiple
implementations
Pass args
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Example:

services.AddSingleton<IMyDep, MyDep>();
Yes Yes No
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})

Examples:

services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep(99));
Yes Yes Yes
Add{LIFETIME}<{IMPLEMENTATION}>()

Example:

services.AddSingleton<MyDep>();
Yes No No
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})

Examples:

services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep(99));
No Yes Yes
AddSingleton(new {IMPLEMENTATION})

Examples:

services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep(99));
No No Yes

For more information on type disposal, see the Disposal of services section.

The framework also provides TryAdd{LIFETIME} extension methods, which register the service only if there isn't already an implementation registered.

In the following example, the call to AddSingleton registers MessageWriter as an implementation for IMessageWriter. The call to TryAddSingleton has no effect because IMessageWriter already has a registered implementation:

services.AddSingleton<IMessageWriter, MessageWriter>();
services.TryAddSingleton<IMessageWriter, DifferentMessageWriter>();

The TryAddSingleton has no effect, as it was already added and the "try" will fail.

For more information, see:

The TryAddEnumerable(ServiceDescriptor) methods register the service only if there isn't already an implementation of the same type. Multiple services are resolved via IEnumerable<{SERVICE}>. When registering services, add an instance if one of the same types hasn't already been added. Library authors use TryAddEnumerable to avoid registering multiple copies of an implementation in the container.

In the following example, the first call to TryAddEnumerable registers MessageWriter as an implementation for IMessageWriter1. The second call registers MessageWriter for IMessageWriter2. The third call has no effect because IMessageWriter1 already has a registered implementation of MessageWriter:

public interface IMessageWriter1 { }
public interface IMessageWriter2 { }

public class MessageWriter : IMessageWriter1, IMessageWriter2
{
}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter2, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());

Service registration is generally order independent except when registering multiple implementations of the same type.

IServiceCollection is a collection of ServiceDescriptor objects. The following example shows how to register a service by creating and adding a ServiceDescriptor:

string secretKey = Configuration["SecretKey"];
var descriptor = new ServiceDescriptor(
    typeof(IMessageWriter),
    _ => new DefaultMessageWriter(secretKey),
    ServiceLifetime.Transient);

services.Add(descriptor);

The built-in Add{LIFETIME} methods use the same approach. For example, see the AddScoped source code.

Constructor injection behavior

Services can be resolved by using:

Constructors can accept arguments that aren't provided by dependency injection, but the arguments must assign default values.

When services are resolved by IServiceProvider or ActivatorUtilities, constructor injection requires a public constructor.

When services are resolved by ActivatorUtilities, constructor injection requires that only one applicable constructor exists. Constructor overloads are supported, but only one overload can exist whose arguments can all be fulfilled by dependency injection.

Scope validation

When the app runs in the Development environment and calls CreateDefaultBuilder to build the host, the default service provider performs checks to verify that:

  • Scoped services aren't resolved from the root service provider.
  • Scoped services aren't injected into singletons.

The root service provider is created when BuildServiceProvider is called. The root service provider's lifetime corresponds to the app's lifetime when the provider starts with the app and is disposed when the app shuts down.

Scoped services are disposed by the container that created them. If a scoped service is created in the root container, the service's lifetime is effectively promoted to singleton because it's only disposed by the root container when the app shuts down. Validating service scopes catches these situations when BuildServiceProvider is called.

See also