So, what is Dependency Injection in C#? Great question.
In modern software development, writing clean, maintainable, and scalable code is the main focus every developer should have. One of the fundamental principles that help achieve this is Dependency Injection (DI). It is a powerful technique that improves the flexibility, testability, and overall design of applications.
This article serves as a precursor to the video lecture in this course that goes into detail about Dependency Injection. If you prefer a more visual explanation, be sure to check out the lecture following this article for a demonstration of these concepts in action.
Let’s start by understanding Dependency Injection in simple terms before diving into its implementation in C#.
Imagine you own a coffee shop. To serve coffee, you need an espresso machine. One way to set up your shop is to buy an espresso machine and have it built into your counter. This means your shop is tightly coupled to that specific machine. If the machine breaks down or if you want to upgrade to a better one, replacing it would be a hassle.
A better approach would be to rent an espresso machine from a supplier. If the machine stops working, you can easily replace it with a different one without modifying your entire coffee counter. This approach is much more flexible.
This is exactly how Dependency Injection works in software development. Instead of a class creating its dependencies (buying and installing a machine), the dependencies are provided externally (renting a machine), making the system more adaptable, testable, and easier to maintain.
Now, let’s see how this applies to programming by looking at a traditional implementation without Dependency Injection.
Let’s consider an example where we don’t use Dependency Injection:
public class EmailService
{
public void SendEmail(string message)
{
Console.WriteLine($"Sending email: {message}");
}
}
public class UserService
{
private EmailService _emailService;
public UserService()
{
_emailService = new EmailService(); // Directly creating the dependency
}
public void RegisterUser(string name)
{
Console.WriteLine($"User {name} registered successfully.");
_emailService.SendEmail($"Welcome {name}!");
}
}
class Program
{
static void Main()
{
UserService userService = new UserService();
userService.RegisterUser("John Doe");
}
}
Tightly Coupled Classes: The UserService directly creates an instance of EmailService. This means if we ever want to change EmailService, we will have to modify UserService as well.
Hard to Test: We cannot easily replace EmailService with a mock service for testing.
Not Scalable: If the application grows and more dependencies are introduced, managing them manually becomes complex.
To fix the above problems, we will use Dependency Injection.
Instead of creating dependencies inside the class, we pass them in through the constructor.
public interface IEmailService
{
void SendEmail(string message);
}
public class EmailService : IEmailService
{
public void SendEmail(string message)
{
Console.WriteLine($"Sending email: {message}");
}
}
public class UserService
{
private readonly IEmailService _emailService;
// Injecting dependency via constructor
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
public void RegisterUser(string name)
{
Console.WriteLine($"User {name} registered successfully.");
_emailService.SendEmail($"Welcome {name}!");
}
}
class Program
{
static void Main()
{
IEmailService emailService = new EmailService();
UserService userService = new UserService(emailService);
userService.RegisterUser("John Doe");
}
}
UserService no longer creates EmailService internally.
Dependencies are now passed into UserService, making it more flexible.
We can now replace EmailService with a mock implementation for testing.
Instead of manually managing dependencies, we can use a Dependency Injection (DI) Container like the built-in DI container in ASP.NET Core.
In an ASP.NET Core application, we configure DI in Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<UserService>();
var app = builder.Build();
// Resolve dependencies
var userService = app.Services.GetRequiredService<UserService>();
userService.RegisterUser("John Doe");
app.Run();
Automatic dependency resolution
Better management of service lifetimes (Singleton, Scoped, Transient)
Reduces boilerplate code
Constructor Injection (Most common & preferred)
Injects dependencies via the constructor.
Property Injection
Dependencies are set through public properties.
public class UserService
{
public IEmailService EmailService { get; set; }
}
Method Injection
Dependencies are passed as method parameters.
public void RegisterUser(string name, IEmailService emailService) { }
So, does this clear up the question? At least we hope that you now have a general idea of what DI is. Naturally, this won´t be enough for you to become an expert in DI as of yet; though for that, we have the following lectures!
Now, with that idea in mind, I believe you are now ready to get into the details and the practice, so make sure you get onto the next lecture.
See you there!