Simplifying Dependency Injection : A Guide to Dependency Injection in C#

Mohamed Hendawy
4 min readDec 20, 2023

--

When building software, it’s common to have classes that rely on other classes to perform certain tasks. How these dependencies are managed can greatly impact the flexibility and maintainability of your code. One approach that helps in achieving a more modular and maintainable design is called Dependency Injection (DI).

What is Dependency Injection?

Dependency Injection is a way of designing software where a class doesn’t create its own dependencies but receives them from the outside. Imagine you have a car (your class), and it needs fuel (dependencies) to run. Instead of the car creating the fuel, you inject it from an external source.

How Dependency Injection Works in C#

1. Constructor Injection:

In simple terms, constructor injection means passing dependencies through the constructor of a class. Let’s take a look at a basic example:

public class UserService
{
private readonly ILogger _logger;

public UserService(ILogger logger)
{
_logger = logger;
}
public void PerformAction()
{
_logger.Log("Action performed");
// Rest of the logic
}
}

2. Dependency Injection Containers:

Think of a Dependency Injection Container as a manager that takes care of providing dependencies when needed. In C#, frameworks like Microsoft.Extensions.DependencyInjection help with this. Here's a snippet:

var serviceProvider = new ServiceCollection()
.AddTransient<ILogger, ConsoleLogger>()
.AddTransient<UserService>()
.BuildServiceProvider();
var userService = serviceProvider.GetService<UserService>();

3. Other Injection Techniques:

Dependencies can also be injected through properties or methods, offering different ways to achieve the same goal.

Property Injection:

In property injection, dependencies are set through public properties. Here’s a simple example:

public class NotificationService
{
// Public property for the dependency
public INotificationProvider NotificationProvider { get; set; }

// Method using the injected dependency
public void SendNotification(string message)
{
// Check if the dependency is available
NotificationProvider?.Send(message);
}
}

In this example, NotificationProvider is a property representing the injected dependency. The SendNotification method utilizes this property to perform the desired action.
NotificationService has a public property (NotificationProvider) that can be set from the outside. This is known as property injection. The class expects that someone external to it will provide the INotificationProvider implementation.

Method Injection:

Method injection involves passing dependencies through methods. Here’s an example:

public class ReportingService
{
// Method with dependency injection
public void GenerateReport(IDataProvider dataProvider)
{
// Get data from the injected dependency
var data = dataProvider.GetData();

// Logic to generate a report using the provided data
Console.WriteLine("Generating report with data: " + data);
}
}

In this case, the GenerateReport method receives an IDataProvider as a parameter. The method then uses the provided dependency to retrieve data and generate a report. This approach allows flexibility in providing different data sources.

Why Dependency Injection?

1. Modularity:

Dependency Injection encourages breaking down your code into smaller, more focused pieces. Each class is responsible for a specific job, making your code modular and easier to understand.

Example: Modularity with Constructor Injection
Consider a simple e-commerce application where you have a OrderService class that depends on a PaymentProcessor:

public class OrderService
{
private readonly IPaymentProcessor _paymentProcessor;

public OrderService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}

public void ProcessOrder(Order order)
{
// Logic to process the order
_paymentProcessor.ProcessPayment(order);
}
}

In this example, OrderService is focused solely on processing orders, while the specific payment processing logic is delegated to the injected IPaymentProcessor. This separation of concerns makes the code more modular and easier to understand.

2. Testability:

With Dependency Injection, testing becomes simpler. You can easily replace real implementations with fake ones during testing, ensuring that each part of your code works as expected.

Example: Testability with Property Injection
Consider a NotificationService that sends notifications, and you want to test it without actually sending notifications. With property injection, you can easily substitute a mock implementation during testing:

public class NotificationService
{
// Public property for the dependency
public INotificationProvider NotificationProvider { get; set; }

// Method using the injected dependency
public void SendNotification(string message)
{
// Check if the dependency is available
NotificationProvider?.Send(message);
}
}

During testing, you can set NotificationProvider to a mock implementation that doesn't send real notifications, allowing you to isolate and test the behavior of NotificationService without affecting external systems.

3. Flexibility:

By injecting dependencies, your classes become more flexible. You can easily swap one implementation for another without changing the code of the dependent class.

Example: Flexibility with Dependency Injection Containers
Using a dependency injection container, such as the one provided by Microsoft.Extensions.DependencyInjection, enhances flexibility. Imagine you have a class PaymentService:

public class PaymentService
{
private readonly IPaymentProcessor _paymentProcessor;

public PaymentService(IPaymentProcessor paymentProcessor)
{
_paymentProcessor = paymentProcessor;
}

public void ProcessPayment(Order order)
{
// Logic to process the payment
_paymentProcessor.ProcessPayment(order);
}
}

Using a dependency injection container:

var serviceProvider = new ServiceCollection()
.AddTransient<IPaymentProcessor, CreditCardPaymentProcessor>()
.AddTransient<PaymentService>()
.BuildServiceProvider();

var paymentService = serviceProvider.GetService<PaymentService>();

Now, you can easily switch the payment processing implementation by changing the registration in the container, promoting flexibility.

4. Reusability:

Classes with clear dependencies can be reused in different parts of your application or even in entirely different projects.

Example: Reusability with Constructor Injection
Consider an EmailService that sends emails. It can be reused in various parts of your application by injecting it where needed:

public class EmailService
{
private readonly IEmailProvider _emailProvider;

public EmailService(IEmailProvider emailProvider)
{
_emailProvider = emailProvider;
}

public void SendEmail(string to, string subject, string body)
{
// Logic to send an email using the injected provider
_emailProvider.SendEmail(to, subject, body);
}
}

Now, you can reuse the EmailService in different classes or modules by injecting the appropriate IEmailProvider implementation. This promotes code reusability across your application.

These examples illustrate that dependency injection can be achieved through various means, and the choice of technique depends on the specific requirements and design preferences of your application. Each approach — constructor injection, property injection, and method injection — offers different ways to structure and manage dependencies in your code.

--

--