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


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
, orSingleton
.
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.
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.