Dependency Injection (DI) is a key technique to follow the Dependency Inversion Principle (DIP) from the SOLID principles. It helps you write cleaner, more testable, and maintainable code by injecting dependencies instead of hardcoding them.
Sounds complex? It’s not! Let’s break it down in just 5 minutes.
What Is Dependency Injection (DI)?
Picture a coffee machine: you don’t fuse the water tank inside, you make it removable.
DI does the same: instead of a class building its own dependencies, you pass them in. This keeps your code flexible and testable.
Step 1: The Problem Without DI
Here’s a class without DI:
public class CoffeeMaker
{
private readonly Logger _logger = new Logger(); // <== The logger is harcoded!
public string BrewCoffee()
{
_logger.Log("Brewing coffee...");
return "Coffee ready!";
}
}
public class Logger
{
public void Log(string message) => Console.WriteLine(message);
}
The issue? CoffeeMaker
is locked to Logger. If you want a different logger or a mock one for testing? You’re stuck rewriting it.
It's like having a coffe machine fused to the water supply.
Step 2: DI Saves the Day
With DI, inject the dependency via the constructor:
public class CoffeeMaker
{
private readonly ILogger _logger;
public CoffeeMaker(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public string BrewCoffee()
{
_logger.Log("Brewing coffee...");
return "Coffee ready!";
}
}
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
}
Now CoffeeMaker
works with any ILogger
implementation, not to a concrete implementation. Flexibility achieved!
Step 3: Swapping for Testing
DI shines in testing. Let’s swap ConsoleLogger
with a mock logger using a simple test (e.g., with xUnit):
public class MockLogger : ILogger
{
public string LastMessage { get; private set; }
public void Log(string message) => LastMessage = message;
}
[Fact]
public void BrewCoffee_LogsCorrectly()
{
// Arrange
var mockLogger = new MockLogger();
var coffeeMaker = new CoffeeMaker(mockLogger);
// Act
var result = coffeeMaker.BrewCoffee();
// Assert
Assert.Equal("Brewing coffee...", mockLogger.LastMessage);
Assert.Equal("Coffee ready!", result);
}
See? We injected a MockLogger
, tested the log message, and didn’t touch CoffeeMaker
. Swap as needed!
Step 4: DI in ASP.NET Core
In C# 9 with ASP.NET Core, the built-in container handles it:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ILogger, ConsoleLogger>();
builder.Services.AddScoped<CoffeeMaker>();
var app = builder.Build();
app.MapGet("/", (CoffeeMaker coffeeMaker) => coffeeMaker.BrewCoffee());
app.Run();
For tests, just register MockLogger
instead of ConsoleLogger
. Easy!
Why This Rocks
- Testability: Mock dependencies like we did above.
- Flexibility: Switch to a
FileLogger
? ImplementILogger
and update the registration. - Clean Code: No hardcoded mess.
Bonus: Manual DI
No framework? Do it yourself:
ILogger logger = new ConsoleLogger();
CoffeeMaker coffeeMaker = new CoffeeMaker(logger);
Console.WriteLine(coffeeMaker.BrewCoffee());
Wrap-Up
DI is just passing dependencies instead of creating them inside classes.
Use interfaces, swap implementations (like mocks for tests), and enjoy cleaner code.
Happy coding! ⚡