Dependency Injection in C#: A Simple Guide for Beginners
Dependency Injection (DI) is one of the key design patterns in modern C# and .NET development. If you are just starting your journey in programming, the term may seem complex, but in reality, it is much simpler. In this article, we will break down what Dependency Injection is, why it is needed, and how to use it in practice.
What is Dependency Injection in Simple Terms?
Imagine you are building a computer. You can buy a ready-made system unit where everything is soldered in and cannot be replaced. Or you can assemble it from individual components: a processor, a graphics card, RAM. The second approach is much more flexible — you can easily replace the graphics card with a more powerful one or add another stick of RAM.
Dependency Injection works on the same principle. Instead of rigidly "soldering" dependencies inside a class (creating objects directly in the code), you "inject" them from the outside. This makes your code flexible, testable, and easy to maintain.
Let's look at a bad example without DI:
public class EmailService{ private SmtpClient _client; public EmailService() { // Tight coupling — the class creates the dependency itself _client = new SmtpClient("smtp.example.com", 587); } public void Send(string to, string message) { _client.Send(to, message); }}Now the same example using DI:
public class EmailService{ private readonly ISmtpClient _client; // Dependency is injected via the constructor public EmailService(ISmtpClient client) { _client = client; } public void Send(string to, string message) { _client.Send(to, message); }}The difference is obvious: in the second case, we work through the ISmtpClient interface, and the specific implementation is passed from the outside.
Main Ways to Inject Dependencies in C#
In C#, there are three main ways to implement DI. Let's look at each of them.
1. Constructor Injection
This is the most popular and recommended method. Dependencies are passed through constructor parameters and stored in private fields.
public class OrderService{ private readonly IEmailService _emailService; private readonly ILogger _logger; public OrderService(IEmailService emailService, ILogger logger) { _emailService = emailService; _logger = logger; } public void ProcessOrder(Order order) { _logger.Log("Order processing started"); _emailService.Send(order.CustomerEmail, "Your order has been received"); _logger.Log("Order processing completed"); }}2. Property Injection
The dependency is set via a public property. This method is convenient when the dependency is optional.
public class ReportGenerator{ public ILogger Logger { get; set; } public void GenerateReport() { Logger?.Log("Generating report..."); // Generation logic }}3. Method Injection
The dependency is passed directly into the method where it is needed. It is usually used rarely but can be useful in some scenarios.
public class DataProcessor{ public void ProcessData(string data, IValidator validator) { if (validator.Validate(data)) { // Data processing } }}Practical Example: From Bad to Good Code
Let's see how DI can improve real code. Suppose we have a class for working with a database.
Bad code (without DI):
public class UserRepository{ private SqlConnect