Alva

Alva

programmer
github
telegram
email

Dependency Injection in dotnet-02. Getting Started Example

This is an example code, all using .NET 6. You can get the specific code in this repository Articles.DI.

Dependency Injection (DI) in .NET is a first-class citizen, and the official framework provides patterns for configuration, logging, and options.

DI solves these problems in the following ways:

  • Abstract dependencies using interfaces or base classes.
  • Register services in a unified container.
  • Inject services into classes that use them, with the framework responsible for instantiation and lifecycle management.

Let's use an example to demonstrate how to use dependency injection in .NET. The following example is a background service called Worker, which depends on MessageWriter and calls MessageWriter.Write to output a simple log every 1 second.

// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService2
// Define the host service
IHost host = Host.CreateDefaultBuilder(args)
   .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
    })
   .Build();

await host.RunAsync();  // Start

// Specific background service class
public class Worker : BackgroundService
{
    // Hard-coded dependency, need to initialize member variables manually
    private readonly MessageWriter _messageWriter = new();

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

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

Using Dependency Injection#

In the previous code, the Worker class explicitly depends on the MessageWriter class and instantiates the corresponding instance in its own code block.

As the complexity of the project increases, if the Worker class contains multiple different variables, each of them needs to be instantiated by the Worker class itself. Moreover, if these variables depend on other services, the initialization code alone will become very complicated. Additionally, if you want to change the implementation of the MessageWriter class without using it anymore, you will also need to modify the code. The coupling between the two classes is very tight.

To decouple them, we can use dependency injection to achieve inversion of control by following these steps:

  1. Abstract the behavior of the MessageWriter class into an interface called IMessageWriter.
  2. Register the service in the container and register the implementation of IMessageWriter as MessageWriter.
  3. Modify the type of _messageWriter to the interface IMessageWriter. The Worker class only depends on the interface.
  4. In the constructor of the Worker class, declare the required type variables and let the container inject them.
IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
        {
            services.AddHostedService<Worker>();
            // 2. Register the service
            services.AddSingleton<IMessageWriter, MessageWriter>();
        })
    .Build();

await host.RunAsync();

// 1. Abstract interface
public interface IMessageWriter
{
    void Write(string message);
}

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


public class Worker : BackgroundService
{
    // 3. Loose coupling, only depend on the interface, not the concrete implementation. The Worker class is not responsible for initializing members.
    private readonly IMessageWriter _messageWriter;


    // 4. Pass the injected concrete instance through the constructor and assign it to the _messageWriter member
    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);
        }
    }
}

In the above code, there is no direct connection between the Worker class and the MessageWriter class. The former has a member of type IMessageWriter, and the container service will inject an instance through the constructor, which the Worker class can use directly without knowing how to construct an instance of that type. The latter, MessageWriter, is a class that implements the IMessageWriter interface. It only needs to implement the corresponding methods according to the interface requirements, without worrying about how others will use it. When registering the service in the container, MessageWriter is registered as the implementation of IMessageWriter. Therefore, when a service depends on IMessageWriter, the container will automatically generate an instance of MessageWriter and inject it.

Dependency injection decouples the two classes. If you want to change the specific implementation of IMessageWriter, you can simply add a new implementation class and modify the registration. You can refer to the following code:

public class LoggingMessageWriter : IMessageWriter
{
    private readonly ILogger<LoggingMessageWriter> _logger;


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


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


// Register the service: ConfigureServices code block
// services.AddSingleton<IMessageWriter, MessageWriter>();
// When registering the service, change the implementation of IMessageWriter without modifying every business code
services.AddSingleton<IMessageWriter, LoggingMessageWriter>();

The Worker class only depends on the IMessageWriter interface and does not care about the implementation details. These details are handled by the framework. All we need to do is write a new implementation class and modify the registration code.

Dependency Injection in .NET

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.