Skip to main content

Dependency Injection (DI)

Level: Beginner to Intermediate

ℹ️ Where This Fits

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.

ℹ️ What You'll Learn
  • Dependency Injection principle: Don't create objects manually, ask the framework to provide them
  • Why DI matters: Easier testing (mock services), cleaner code (no new everywhere), loose coupling
  • Service registration: builder.Services.AddScoped<IStudentService, StudentService>()
  • Constructor injection: public MyController(IStudentService service) receives service automatically
  • Transient lifetime: New instance every time (use for stateless utilities)
  • Scoped lifetime: New instance per HTTP request (use for DbContext, services working with data)
  • Singleton lifetime: 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: IStudentService interface → StudentService implementation 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.Services acts 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.

ProblemWhy It Hurts
Hard-coded object creationYou cannot easily replace the service
Difficult testingYou cannot inject a fake service
Hidden dependenciesOther developers cannot see what the class needs
Tight couplingController 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.

💻 Try It — Console App
💡 Register services before builder.Build().⌥ GitHub
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<IAttendanceService, AttendanceService>();
builder.Services.AddSingleton<ISchoolYearProvider, SchoolYearProvider>();

var app = builder.Build();

app.MapControllers();

app.Run();

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.

LifetimeMeaningCommon Use
TransientNew object every time it is requestedLightweight stateless helpers
ScopedOne object per HTTP requestDatabase context, business services
SingletonOne object for the whole applicationApp-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

ℹ️ Important 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

MistakeBetter Approach
Creating services with new inside controllersRegister and inject services
Registering everything as singletonChoose lifetime carefully
Injecting too many services into one controllerSplit responsibilities
Putting business logic in Program.csMove logic into services
Injecting IConfiguration everywhereUse 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:

  1. Create IStudentService.
  2. Create StudentService.
  3. Register it using AddScoped.
  4. Inject it into a controller.
  5. Return a student by id.

Quick Recap

QuestionAnswer
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
🎯 Interview Favourite

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 AI to Learn Faster

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.

Next Article

-> Configuration and appsettings.json

nexcoding.in