Dependency Injection in C#: Writing Clean, Modular, and Testable Code

Developer FabioDeveloper Fabio
2 min read

Dependency Injection (DI) is a technique that makes this possible: it separates the creation of dependencies from the business logic, improving modularity, maintainability, and testability.


What Is Dependency Injection?

It's a pattern where an object’s dependencies are provided (injected) from the outside, rather than having the object create or locate them itself.

Before (without DI):

public class OrderService
{
    private readonly EmailService _emailService = new EmailService();

    public void PlaceOrder(Order order)
    {
        // order logic...
        _emailService.SendConfirmation(order);
    }
}

Here, OrderService depends directly on EmailService and creates it internally. This makes it hard to replace or test.


With Dependency Injection

public class OrderService
{
    private readonly IEmailService _emailService;

    public OrderService(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public void PlaceOrder(Order order)
    {
        // order logic...
        _emailService.SendConfirmation(order);
    }
}

The IEmailService interface is passed in from the outside (for example, by a DI container).


Benefits of DI

Decoupling: OrderService doesn't know the concrete implementation of IEmailService.
Testability: You can pass in a mock or fake IEmailService in tests.
Flexibility: You can switch implementations without changing OrderService.
Simplified maintenance: Dependencies are clear and managed centrally.


Types of Dependency Injection

  • Constructor Injection (most common): Dependencies are passed through the constructor.

  • Property Injection: Dependencies are set via public properties.

  • Method Injection: Dependencies are passed as method parameters.


DI with ASP.NET Core’s Built-In Container

ASP.NET Core includes a simple built-in DI container.

Service registration in Startup.cs or Program.cs:

services.AddTransient<IEmailService, EmailService>();
services.AddScoped<IOrderRepository, OrderRepository>();

Usage in controllers or classes:

public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
}

Practical Tips

  • Always inject interfaces or abstract classes, not concrete implementations.

  • Avoid injecting too many services into a single class — it may indicate an SRP violation.

  • Prefer constructor injection for clarity and immutability.

  • Set the appropriate lifetime: Transient, Scoped, or Singleton.


Testability with DI

In unit tests, you can easily replace dependencies with mocks:

var mockEmail = new Mock<IEmailService>();
var orderService = new OrderService(mockEmail.Object);

Conclusion

Dependency Injection is a powerful ally in writing clean, modular, and easily testable C# code.
If you’re not using it yet, start introducing it gradually — and you’ll see a significant improvement in code quality.

In the next article, we’ll explore how to write testable code and best practices for unit testing.

0
Subscribe to my newsletter

Read articles from Developer Fabio directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Developer Fabio
Developer Fabio

I'm a fullstack developer and my stack is includes .net, angular, reactjs, mondodb and mssql I currently work in a little tourism company, I'm not only a developer but I manage a team and customers. I love learning new things and I like the continuous comparison with other people on ideas.