Dependency Injection in C#: Practical Guide with Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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.



Step 1: Install the package

Blogs

Book Recommendations