Dependency Injection
Dependency Injection (DI) is a design pattern where a class receives the objects it needs from the outside, rather than creating them itself.[1] Instead of a class saying "I will build my own database connection", it says "give me a database connection and I will use it." The thing being given is called a dependency, and the act of giving it is the injection.
This might sound like a small change, but it has a big effect. When a class creates its own dependencies, it is tightly coupled to them — you cannot swap them out, mock them in tests, or change them without touching the class itself. DI breaks that coupling. The class just works with whatever it receives, making your code easier to test, change, and maintain.
The Problem It Solves
Imagine a UserService that needs to send emails:
// Without DI — tightly coupled
public class UserService
{
private EmailSender _emailSender = new EmailSender(); // hardcoded
public void Register(string email)
{
// do registration...
_emailSender.Send(email, "Welcome!");
}
}
UserService is now locked to EmailSender. You cannot test UserService without actually sending emails. You cannot swap EmailSender for a different provider without editing UserService.
With DI, you pass the dependency in:
// With DI — loosely coupled
public class UserService
{
private readonly IEmailSender _emailSender;
public UserService(IEmailSender emailSender) // injected
{
_emailSender = emailSender;
}
public void Register(string email)
{
// do registration...
_emailSender.Send(email, "Welcome!");
}
}
Now UserService only knows about the IEmailSender interface, not the concrete class. In tests, you pass in a fake. In production, you pass in the real one.
Types of Injection
There are three ways to inject a dependency:
- Constructor injection: passed through the class constructor. This is the most common and preferred way. Dependencies are clear and required upfront.
- Property injection: set via a public property after construction. Useful when a dependency is optional, but makes it less obvious what a class needs.
- Method injection: passed as a parameter to a specific method. Used when only one method needs the dependency.
Constructor injection is the default in .NET and most frameworks because it makes dependencies explicit and guarantees the class is never in a half-built state.
The DI Container
In a large app, wiring up every dependency by hand would be tedious. A DI container (also called an IoC container) automates this. You register your services once, and the container figures out how to build them and pass them around.
In ASP.NET Core, registration happens in Program.cs:
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
When a controller or service asks for IEmailSender, the container automatically injects SmtpEmailSender. You never call new yourself.
Why It Matters for Testing
The biggest practical win from DI is testability. Because you inject interfaces, you can swap real implementations for fakes in unit tests:
// In a test
var fakeEmailSender = new FakeEmailSender();
var userService = new UserService(fakeEmailSender);
userService.Register("test@example.com");
Assert.True(fakeEmailSender.WasCalled);
No real emails sent. No database hit. The test is fast, isolated, and predictable.
Service Lifetimes
When registering services with a DI container, you also choose how long each instance lives. See DI Service Lifetimes for the full breakdown of Transient, Scoped, and Singleton.