Dependency Injection: A Simple and Clear Guide

Published on: March 10, 2025

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? Implement ILogger 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! ⚡


Dependency InjectionOOPSOLID PrinciplesC#Software Engineering