Introduction: What is Dependency Injection and Why You Need It
Dependency Injection (DI) is one of the key design patterns that implements the Dependency Inversion Principle from SOLID. In practice, this means that objects do not create their dependencies themselves but receive them from the outside. This makes the code flexible, testable, and loosely coupled.
In the C# and .NET ecosystem, DI has become the de facto standard. Starting with .NET Core, the platform includes its own dependency injection container, which is used in ASP.NET Core, background services, and even console applications. Understanding DI is essential for modern C# development.
In this article, we will break down DI using real examples: from manual injection to using the built-in .NET container. You will see how to eliminate tight coupling and make code suitable for unit testing.
The Problem Without Dependency Injection
Let's imagine a typical scenario: the OrderService class processes orders and sends email notifications. Without DI, the code looks like this:
public class EmailSender{ public void Send(string to, string subject, string body) { Console.WriteLine($"Sending email to {to}: {subject}"); }}
public class OrderService{ private EmailSender _emailSender;
public OrderService() { // Tight coupling — the class creates its own dependency _emailSender = new EmailSender(); }
public void ProcessOrder(int orderId) { // Order processing logic... _emailSender.Send("user@example.com", "Order Placed", $"Your order #{orderId} has been received."); }}What's the problem? OrderService is tightly coupled to a specific implementation of EmailSender. If we want to replace email sending with SMS or logging, we would have to change the OrderService code. Testing also becomes more difficult: it's impossible to replace the real EmailSender with a mock object.
Solution: Constructor Injection
The first step towards DI is to extract an abstraction (interface) and pass the dependency through the constructor. This is called Constructor Injection.
Step 1: Create an interface
public interface INotificationService{ void Send(string to, string subject, string body);}Step 2: Implement the interface
public class EmailSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"Email sent: {subject} -> {to}"); }}
public class SmsSender : INotificationService{ public void Send(string to, string subject, string body) { Console.WriteLine($"SMS sent: {subject} -> {to}"); }}Step 3: Inject the dependency via constructor
public class OrderService{ private readonly INotificationService _notificationService;
// Dependency is passed from the outside public OrderService(INotificationService notificationService) { _notificationService = notificationService; }
public void ProcessOrder(int orderId) { // Order processing logic... _notificationService.Send("user@example.com", "Order Placed", $"Your order #{orderId} has been received."); }}Now OrderService doesn't know which specific implementation is being used. We can plug in EmailSender, SmsSender, or any other class that implements INotificationService. The code has become flexible and testable.
Using the Built-in IoC Container in .NET
Manually creating all dependencies is inconvenient. .NET has a built-in container (IoC container) that automatically manages the lifecycle of objects. Let's look at an example for a console application.