Dependency Injection (DI)
Level: Beginner to Intermediate
Dependency Injection is how ASP.NET Core gives services to controllers, minimal API handlers, middleware, filters, and background jobs. Learn this before building real APIs with database access.
- Dependency Injection principle: Don't create objects manually, ask the framework to provide them
- Why DI matters: Easier testing (mock services), cleaner code (no
neweverywhere), loose coupling - Service registration:
builder.Services.AddScoped<IStudentService, StudentService>() - Constructor injection:
public MyController(IStudentService service)receives service automatically Transientlifetime: New instance every time (use for stateless utilities)Scopedlifetime: New instance per HTTP request (use for DbContext, services working with data)Singletonlifetime: Single instance for entire application (use for configuration, logging, caches)- Choosing correct lifetime: Wrong choice causes bugs (Singleton DbContext = shared data across requests)
- Interfaces for services:
IStudentServiceinterface →StudentServiceimplementation pattern - Container resolves dependencies automatically: If StudentController needs IStudentService and ILogger, DI provides both
- School Management example: StudentService injected into StudentsController → controller calls service → service queries database
- Common mistakes: Singleton DbContext (data sharing), forgetting to register service (null reference), circular dependencies
- Dependency injection containers: How
builder.Servicesacts as registry and factory
The Simple Meaning
Dependency Injection means:
Do not create required objects manually inside your class.
Ask ASP.NET Core to provide them.
In a school application, a StudentController may need:
- student database service
- attendance service
- logger
- configuration
- email service
Without DI, the controller creates everything by itself. With DI, ASP.NET Core creates those objects and gives them to the controller.
Problem Without DI
public class StudentController
{
private readonly StudentService _studentService;
public StudentController()
{
_studentService = new StudentService();
}
}
This works for a small demo, but it becomes a problem in real projects.
| Problem | Why It Hurts |
|---|---|
| Hard-coded object creation | You cannot easily replace the service |
| Difficult testing | You cannot inject a fake service |
| Hidden dependencies | Other developers cannot see what the class needs |
| Tight coupling | Controller depends directly on implementation details |
Better Code With DI
public class StudentController : ControllerBase
{
private readonly IStudentService _studentService;
public StudentController(IStudentService studentService)
{
_studentService = studentService;
}
}
Now the controller does not create StudentService. It simply declares what it needs.
Registering Services
Services are registered in Program.cs.
Important order:
Register services -> Build app -> Use services
Once builder.Build() is called, the service container is created.
Constructor Injection
Constructor injection is the most common DI style in ASP.NET Core.
[ApiController]
[Route("api/students")]
public class StudentsController : ControllerBase
{
private readonly IStudentService _studentService;
private readonly ILogger<StudentsController> _logger;
public StudentsController(
IStudentService studentService,
ILogger<StudentsController> logger)
{
_studentService = studentService;
_logger = logger;
}
[HttpGet("{id}")]
public IActionResult GetStudent(int id)
{
_logger.LogInformation("Fetching student {StudentId}", id);
var student = _studentService.GetStudent(id);
if (student == null)
{
return NotFound();
}
return Ok(student);
}
}
ASP.NET Core sees the constructor and automatically provides registered dependencies.
Service Lifetimes
A lifetime decides how long a service object lives.
| Lifetime | Meaning | Common Use |
|---|---|---|
Transient | New object every time it is requested | Lightweight stateless helpers |
Scoped | One object per HTTP request | Database context, business services |
Singleton | One object for the whole application | App-wide cache, settings provider |
Transient
Use Transient when the service is small and has no request-specific state.
builder.Services.AddTransient<IReportFormatter, PdfReportFormatter>();
Every time ASP.NET Core asks for IReportFormatter, a new object is created.
Scoped
Use Scoped for most business services in web applications.
builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<SchoolDbContext>();
For one HTTP request, the same instance is reused.
Example:
GET /api/students/101
During this one request, StudentService and SchoolDbContext remain consistent.
Singleton
Use Singleton only when one shared instance is safe.
builder.Services.AddSingleton<ISchoolCalendarCache, SchoolCalendarCache>();
Good singleton examples:
- read-only cache
- application settings provider
- expensive shared object that is thread-safe
Avoid storing request-specific data in a singleton.
The Most Important Lifetime Rule
A singleton service should not depend on a scoped service. A singleton lives for the whole app, but a scoped service lives only for one request.
Bad:
builder.Services.AddScoped<SchoolDbContext>();
builder.Services.AddSingleton<ReportCache>();
If ReportCache tries to use SchoolDbContext, it can cause lifetime problems.
Interface and Implementation
Most real projects register an interface and implementation.
builder.Services.AddScoped<IStudentService, StudentService>();
Meaning:
When someone asks for IStudentService,
give them StudentService.
Interface:
public interface IStudentService
{
StudentDto? GetStudent(int id);
List<StudentDto> GetAllStudents();
}
Implementation:
public class StudentService : IStudentService
{
public StudentDto? GetStudent(int id)
{
return new StudentDto(id, "Anika", "Grade 8");
}
public List<StudentDto> GetAllStudents()
{
return new List<StudentDto>
{
new StudentDto(101, "Anika", "Grade 8"),
new StudentDto(102, "Rahul", "Grade 9")
};
}
}
DTO:
public record StudentDto(int Id, string Name, string Grade);
DI in Minimal APIs
Minimal APIs can receive services directly in handler parameters.
app.MapGet("/students/{id}", (int id, IStudentService studentService) =>
{
var student = studentService.GetStudent(id);
return student is null ? Results.NotFound() : Results.Ok(student);
});
ASP.NET Core knows id comes from the route and studentService comes from DI.
DI in Controllers
Controllers usually use constructor injection.
public class AttendanceController : ControllerBase
{
private readonly IAttendanceService _attendanceService;
public AttendanceController(IAttendanceService attendanceService)
{
_attendanceService = attendanceService;
}
}
This keeps actions clean.
Why DI Helps Testing
Because the controller asks for IStudentService, tests can provide a fake implementation.
public class FakeStudentService : IStudentService
{
public StudentDto? GetStudent(int id)
{
return new StudentDto(id, "Test Student", "Grade 10");
}
public List<StudentDto> GetAllStudents()
{
return new List<StudentDto>();
}
}
This is much easier than connecting to a real database during every unit test.
Common Mistakes
| Mistake | Better Approach |
|---|---|
Creating services with new inside controllers | Register and inject services |
| Registering everything as singleton | Choose lifetime carefully |
| Injecting too many services into one controller | Split responsibilities |
Putting business logic in Program.cs | Move logic into services |
Injecting IConfiguration everywhere | Use strongly typed options for complex settings |
Beginner Rule of Thumb
For most beginner ASP.NET Core APIs:
Controllers -> Scoped services -> Scoped database context
Use Scoped for most services unless you clearly know why another lifetime is needed.
Practice Task
Create a small school service:
- Create
IStudentService. - Create
StudentService. - Register it using
AddScoped. - Inject it into a controller.
- Return a student by id.
Quick Recap
| Question | Answer |
|---|---|
| What is DI? | Providing dependencies instead of creating them manually |
| Where are services registered? | builder.Services in Program.cs |
| Most common injection type? | Constructor injection |
| Best lifetime for database-related services? | Scoped |
| Lifetime for app-wide shared cache? | Singleton |
Q: What is dependency injection in ASP.NET Core?
Good Answer: "Dependency injection is a built-in ASP.NET Core feature where required services are registered in the DI container and provided to classes automatically. Instead of creating dependencies using new, classes receive them through constructors or handler parameters. This reduces coupling, improves testability, and makes service lifetime management easier. ASP.NET Core supports Transient, Scoped, and Singleton lifetimes."
Use ChatGPT, Claude, or Copilot to go deeper on Dependency Injection. Try these prompts:
"Explain dependency injection using a school application example.""When should I use Scoped instead of Singleton in ASP.NET Core?""Show me a controller that injects a service and ILogger.""What mistakes should beginners avoid with DI lifetimes?"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.