.NET Dependency Injection Explained: A Beginner's Guide
An introduction to the built-in dependency injection (DI) container in .NET. Learn what DI is, why it's important, and how to register and inject services in your ASP.NET Core applications.
If you've started building applications with ASP.NET Core, you've already been using one of its most powerful features, perhaps without even realizing it: Dependency Injection (DI). DI is a software design pattern that allows us to build loosely coupled code, making our applications more maintainable, testable, and flexible.
ASP.NET Core is built from the ground up with a simple but powerful built-in DI container.
The Problem: Tight Coupling
Let's look at a simple example without dependency injection.
public class ReportController
{
private readonly ReportGenerator _reportGenerator;
public ReportController()
{
// The controller is responsible for creating its own dependency.
_reportGenerator = new ReportGenerator();
}
public void GenerateReport()
{
var report = _reportGenerator.CreateReport();
// ...
}
}
In this example, ReportController
is tightly coupled to the ReportGenerator
class. It knows how to create an instance of it. This creates several problems:
- Inflexibility: What if we want to use a different report generator, like
PdfReportGenerator
? We would have to change the code insideReportController
. - Difficult to Test: When we write a unit test for
ReportController
, we are forced to also use a realReportGenerator
. We can't easily substitute a fake or mock version for testing purposes.
The Solution: Inversion of Control and Dependency Injection
Dependency Injection is a form of Inversion of Control (IoC). Instead of a class creating its own dependencies, the dependencies are "injected" from the outside.
We can achieve this by coding against interfaces (abstractions) instead of concrete classes.
1. Define an Interface
public interface IReportGenerator
{
string CreateReport();
}
public class ReportGenerator : IReportGenerator
{
public string CreateReport() => "This is a standard report.";
}
2. Use the Interface in the Controller
Now, the controller depends on the IReportGenerator
interface, not the concrete class. The dependency is passed in through the constructor.
public class ReportController
{
private readonly IReportGenerator _reportGenerator;
// The dependency is "injected" here.
public ReportController(IReportGenerator reportGenerator)
{
_reportGenerator = reportGenerator;
}
public void GenerateReport()
{
var report = _reportGenerator.CreateReport();
// ...
}
}
Now our ReportController
is loosely coupled. It doesn't know or care about the concrete implementation of the report generator. This makes it easy to test and easy to change.
The Role of the DI Container
But who is responsible for creating the ReportGenerator
and passing it to the ReportController
? That's the job of the DI container.
In an ASP.NET Core application, you register your services with the DI container in your Program.cs
file. This is called service registration.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Register our service. We're telling the container that whenever
// someone asks for an IReportGenerator, it should provide an
// instance of ReportGenerator.
builder.Services.AddScoped<IReportGenerator, ReportGenerator>();
var app = builder.Build();
// ...
When a request comes in for a controller, the framework asks the DI container to create an instance of ReportController
. The container sees that the ReportController
's constructor needs an IReportGenerator
. It then looks at its registrations, sees that IReportGenerator
is mapped to ReportGenerator
, creates an instance of ReportGenerator
, and passes it to the controller's constructor.
This all happens automatically. This process is called dependency resolution.
Service Lifetimes
When you register a service, you also have to specify its lifetime. This tells the container how to manage the creation and disposal of the service.
There are three main lifetimes:
AddTransient()
: A new instance of the service is created every time it is requested. Best for lightweight, stateless services.AddScoped()
: A new instance is created once per client request (scope). This is the most common lifetime for services in a web application. For example, your database context should usually be scoped.AddSingleton()
: A single instance is created the first time it is requested, and that same instance is used for the entire lifetime of the application. Best for services that are expensive to create and are thread-safe.
Conclusion
Dependency Injection is a fundamental pattern for building modern .NET applications. By letting the DI container manage the creation of your dependencies, you can write code that is:
- Loosely Coupled: Components don't have direct knowledge of each other's concrete implementations.
- More Testable: It's easy to substitute mock implementations of your dependencies in your unit tests.
- More Maintainable: It's easier to change or replace implementations without affecting the rest of your application.
By mastering the simple concepts of service registration, constructor injection, and service lifetimes, you can leverage the full power of the built-in DI container in ASP.NET Core.