Dependency Injection in C#
Dependency Injection (DI) is a design pattern in which a class receives the objects it needs (its dependencies) from outside, instead of creating them itself with new. It is the most widely used pattern in modern .NET: ASP.NET Core is built around it, and almost every class in a professional ASP.NET Core project gets its services through dependency injection.
In this article you will see the problem DI solves, the three ways to inject a dependency, how to use the container built into ASP.NET Core step by step, how DI makes unit testing easy, and the two errors every developer meets sooner or later. All outputs and error messages below come from running the example application on .NET 8.
What is a dependency?
If class A uses class B to do its work, then B is a dependency of A. A controller that loads students from a service depends on that service. A service that sends email depends on an SMTP client. Dependencies are normal; the question is who creates them.
Think of a mobile phone and its SIM card. The phone doesn't manufacture its own SIM; it has a slot that accepts a SIM from any operator. You can switch operators without buying a new phone. In code, the slot is an interface, the SIM is the implementation, and dependency injection is the act of inserting the SIM from outside.
The problem: tight coupling
Here is a controller that creates its own dependency:
public class StudentController : Controller
{
private readonly StudentService _studentService = new StudentService(); // creates its own dependency
public IActionResult Index() => View(_studentService.GetStudents());
}
This works, but it causes real problems as the application grows:
- Hard to change: to load students from a different source (a new database, an external API), you must edit the controller.
- Hard to test: a unit test of the controller always runs the real
StudentService, and with it the real database. - Hidden requirements: nothing in the controller's public shape says what it needs; you discover it only by reading the code.
- Ripple effect: if
StudentServicelater needs a constructor parameter (say, a connection string), every class that writesnew StudentService()must change.
The controller is tightly coupled to one concrete class. Dependency injection removes that coupling.
The solution: depend on an interface, receive the object
public class StudentController : Controller
{
private readonly IStudentService _studentService;
// The dependency is given to the controller from outside
public StudentController(IStudentService studentService)
{
_studentService = studentService;
}
public IActionResult Index() => View(_studentService.GetStudents());
}
Now the controller only knows the interface IStudentService. Which class implements it, and how that object is created, is decided in one central place. In ASP.NET Core, that place is Program.cs, and the object that creates and supplies dependencies is called the DI container (or IoC container).
This applies the Dependency Inversion Principle: high-level code depends on abstractions, not on concrete details.
The three types of dependency injection
1. Constructor injection (recommended)
Dependencies are passed as constructor parameters, as in the controller above.
- The object can't be created without its dependencies, so it is always in a valid state.
- Dependencies are clearly visible in the constructor.
- Fields can be
readonly, so they can't be replaced later.
This is the standard choice, and the one ASP.NET Core uses for controllers, services, middleware and Razor Pages.
2. Property (setter) injection
public class ReportGenerator
{
public ILogger? Logger { get; set; } // optional dependency, set from outside
public void Generate()
{
Logger?.LogInformation("Generating report");
// ...
}
}
var generator = new ReportGenerator { Logger = logger };
Useful only for optional dependencies that have a sensible default. The object can exist without the dependency, so the code must check for null. The built-in ASP.NET Core container does not perform property injection.
3. Method injection
public class InvoiceService
{
// The dependency is needed only for this one operation
public void Send(Invoice invoice, IEmailSender emailSender)
{
emailSender.Send(invoice.CustomerEmail, "Your invoice", invoice.ToString());
}
}
The caller passes the dependency to the method that needs it. Good when only one method needs the dependency, or when it changes from call to call.
| Type | How it's supplied | Use it for |
|---|---|---|
| Constructor injection | Constructor parameters | Required dependencies (almost always) |
| Property injection | Public settable property | Optional dependencies with a default |
| Method injection | Method parameter | A dependency needed by one method only |
Dependency injection in ASP.NET Core, step by step
Step 1: The model
public class Student
{
public int StudentId { get; set; }
public string StudentName { get; set; } = "";
public int StudentAge { get; set; }
}
Step 2: The interface (the "slot")
public interface IStudentService
{
List<Student> GetStudents();
}
Step 3: The implementation
public class StudentService : IStudentService
{
public List<Student> GetStudents() =>
[
new() { StudentId = 1, StudentName = "Riya", StudentAge = 22 },
new() { StudentId = 2, StudentName = "Aman", StudentAge = 24 },
new() { StudentId = 3, StudentName = "Neha", StudentAge = 21 },
];
}
The data is hard-coded to keep the example short. In a real project this class would read from a database, through Entity Framework Core or a repository.
Step 4: Register the service in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// "When someone asks for IStudentService, give them a StudentService"
builder.Services.AddScoped<IStudentService, StudentService>();
var app = builder.Build();
app.MapDefaultControllerRoute();
app.Run();
builder.Services is the IServiceCollection: the list of services the container knows about. AddScoped also sets the service's lifetime (how long one instance is reused). The three lifetimes, Transient, Scoped and Singleton, are explained in detail in AddTransient vs AddScoped vs AddSingleton.
Step 5: Ask for it in the controller
public class StudentController : Controller
{
private readonly IStudentService _studentService;
public StudentController(IStudentService studentService) => _studentService = studentService;
public IActionResult Index() => View(_studentService.GetStudents());
}
When a request for /Student arrives, ASP.NET Core creates the controller. It sees the constructor needs an IStudentService, looks up the registration, creates a StudentService and passes it in. You never write new StudentService() anywhere.
| StudentId | StudentName | StudentAge |
|---|---|---|
| 1 | Riya | 22 |
| 2 | Aman | 24 |
| 3 | Neha | 21 |
Other places ASP.NET Core can inject
// Into a single action, with [FromServices] (method injection)
public IActionResult Count([FromServices] IStudentService service)
=> Content($"Total students: {service.GetStudents().Count}");
// Into a Razor view
@inject IStudentService StudentService
<p>We have @StudentService.GetStudents().Count students.</p>
// Into minimal API handlers
app.MapGet("/api/students", (IStudentService service) => service.GetStudents());
Why it matters: testing and change
Easy unit testing
Because the controller depends on an interface, a test can give it a fake implementation, with no database involved:
public class FakeStudentService : IStudentService
{
public List<Student> GetStudents() => [ new() { StudentId = 99, StudentName = "Test", StudentAge = 20 } ];
}
[Fact]
public void Index_returns_the_students_from_the_service()
{
var controller = new StudentController(new FakeStudentService());
var result = controller.Index() as ViewResult;
var students = Assert.IsType<List<Student>>(result!.Model);
Assert.Single(students);
}
Mocking libraries such as Moq do the same without writing a fake class by hand.
Easy change
When the students move to a new database, write SqlStudentService : IStudentService and change one line in Program.cs:
builder.Services.AddScoped<IStudentService, SqlStudentService>();
No controller changes, no retesting of the controllers.
Benefits of dependency injection
| Benefit | Why |
|---|---|
| Loose coupling | Classes depend on interfaces, not on concrete classes |
| Testability | Fakes and mocks can replace real services in unit tests |
| Maintainability | Implementations are swapped in one place (Program.cs) |
| Clear dependencies | A constructor lists everything a class needs |
| Lifetime management | The container creates, shares and disposes objects according to their lifetime |
| Consistency | Framework services (logging, configuration, HttpClient, DbContext) are obtained the same way |
Two errors every developer meets
1. Forgetting to register a service
If the AddScoped<IStudentService, StudentService>() line is missing, the request fails with HTTP 500 and this exception:
InvalidOperationException: Unable to resolve service for type 'IStudentService' while attempting to activate 'StudentController'.
The message names both the missing service and the class that needed it. The fix is to register the service in Program.cs.
2. A singleton that depends on a scoped service
A singleton lives for the whole application, so if it receives a scoped service, it would keep that one instance forever (a "captive dependency"). In the Development environment, ASP.NET Core validates registrations and refuses to start:
System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: ReportCache Lifetime: Singleton ImplementationType: ReportCache': Cannot consume scoped service 'IScopedOperation' from singleton 'ReportCache'.)
This and the right fix are explained in Service Lifetimes.
Good practices
- Prefer constructor injection and keep the injected fields
private readonly. - Depend on interfaces for services that talk to databases, files, email or external APIs.
- Keep constructors small. If a class needs seven or more dependencies, it is probably doing too much; split it (Single Responsibility Principle).
- Avoid the service locator pattern: calling
HttpContext.RequestServices.GetService<T>()inside methods hides dependencies and makes testing harder. - Don't inject everything: plain data classes (models, DTOs) are created with
newas usual. - When one interface has several implementations, see Keyed Services Dependency Injection (.NET 8).
Interview questions
1. What is dependency injection?
A design pattern in which a class receives its dependencies from outside, usually through its constructor, instead of creating them itself. It produces loosely coupled, testable code.
2. What are the types of dependency injection?
Constructor injection (recommended for required dependencies), property injection (for optional dependencies) and method injection (for dependencies needed by a single method). The built-in ASP.NET Core container supports constructor injection and, through [FromServices], action-method injection.
3. What is the difference between DI, DIP and IoC?
DIP (Dependency Inversion Principle) is the design rule "depend on abstractions". IoC (Inversion of Control) is the general idea that a framework, not your code, controls object creation and flow. DI is the technique that implements both by supplying dependencies from outside, usually through a DI container.
4. Where are services registered in ASP.NET Core?
In Program.cs, on builder.Services (an IServiceCollection), using AddTransient, AddScoped or AddSingleton, before builder.Build() is called.
5. What happens if a service is not registered?
Activating the class that needs it throws InvalidOperationException: Unable to resolve service for type '...' while attempting to activate '...'.
6. How does DI help unit testing?
Because classes depend on interfaces passed into their constructors, a test can pass a fake or mock implementation and test the class in isolation, without databases or external services.
Summary
- Dependency injection gives a class its dependencies from outside instead of creating them with
new. - Use constructor injection for required dependencies; property and method injection only in special cases.
- In ASP.NET Core, register services in
Program.csand request them in constructors. - DI makes code loosely coupled, testable and easy to change.
- Unregistered services and singletons that capture scoped services are the two most common DI errors.