Dependent Service Implementation Example
public class MessageWriter
{
public void WriteMessage(string message) =>
Console.WriteLine($"MessageWriter.WriteMessage called. Message: {message}");
}
If a class creates an instance of MessageWriter
then, MessageWriter
becomes a dependency of that 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);
}
}
}
Worker
creates, and directly depends on, MessageWriter
. This is problematic because:
- To replace
MessageWriter
with another implementation, you must also modifyWorker
. - If
MessageWriter
has dependencies, they must also be configured inWorker
. - The implementation is difficult to unit test.
Dependency Injection addresses these problems by:
- Using an interface or base class to abstract the dependency implementation.
- Registering the dependency in a service container.
- In .NET, this is IServiceProvider.
- Services are typically registered at app startup and appended to an IServiceCollection.
- Once all services are added, BuildServiceProvider is used to create the service container.
- Injecting the service into the constructor of the class where it is used.
Non-Dependent Service Implementation Example
Consider this implementation instead:
`IMessageWriter.cs`:
public interface IMessageWriter
{
void Write(string message);
}
// The concrete type `MessageWriter` implements this interface:
public class MessageWriter : IMessageWriter
{
public void Write(string message) => Console.WriteLine($"Message: {message}");
}
Worker.cs
:
public class Worker : BackgroundService
{
private readonly IMessageWriter _messageWriter;
// This is *injection* of the *dependency*. The dependency is injected into the constructor of the class
// where it's used:
public Worker(IMessageWriter messageWriter) => _messageWriter = messageWriter;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequest)
{
_messageWriter.Write($"Worker service running.");
await Task.Delay(1000, stoppingToken);
}
}
}
Program.cs
:
var builder = Host.CreateDefaultBuilder(args); // The HostBuilder with a default configuration.
builder.ConfigureServices(
services =>
services
// Add the worker as a hosted service:
.AddHostedService<Worker>()
// Add the IMessageWriter interface as a scoped service with a corresponding concrete class:
.AddScoped<IMessageWriter, MessageWriter>());
var host = builder.Build();
host.Run();
Notes:
- The
Worker
services does not create an instance ofMessageWriter
. The instance is created by the DI container. - The DI container also disposes of this instance when it is no longer used.
- Because the worker service uses the interface, not the concrete type, this makes it easy to change the implementation that the controller uses without modifying the controller.
If another implementation of IMessageWriter
, say, LoggingMessageWriter
, were created…
IMessageWriter.cs
:
public class LoggingMessageWriter : IMessageWriter
{
private readonly ILogger<LoggingMessageWriter> _logger;
// This method depends on `ILogger<T>` (which is a framework-provided service), so it is injected into its
// constructor. The service is automatically registered with CreateDefaultBuilder.
public LoggingMessageWriter(ILogger<LoggingMessageWriter> logger) =>
_logger = logger;
public void Write(string message) =>
_logger.LogInformation(message);
}
…the ConfigureServices method would simply have to register this new implementation:
Program.cs
:
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
services.AddHostedService<Worker>()
.AddScoped<IMessageWriter, LoggingMessageWriter>());