What is Dependency Injection in ASP.NET Core?

ASP.NET Core Dependency Injection Intermediate 2–3 Yrs3–5 Yrs Conceptual 3 min read

Quick Answer

Dependency Injection (DI) is a design technique where a class receives the objects it depends on from the outside — usually through its constructor — instead of creating them itself. ASP.NET Core has a built-in DI container: you register services in IServiceCollection (builder.Services), and the framework constructs your controllers and services for you, passing in whatever they ask for.

Dependency Injection is a design technique that helps us achieve loose coupling. Instead of a class creating its dependencies with new, we provide them from outside, usually through the constructor. In ASP.NET Core there's a container built into the framework: I register my services in Program.cs — say, AddScoped<IEmployeeService, EmployeeService> — and then any controller or service that asks for an IEmployeeService in its constructor gets one automatically. The framework walks the constructor, builds the whole object graph, and hands it to me. The payoff is that my classes depend on interfaces, not concrete types, so they're easy to test with a fake and easy to change without touching the consumers.

What it is. A class declares its dependencies as constructor parameters, typically as interfaces. Something else — the DI container — is responsible for creating those dependencies and supplying them. The class never uses new for its collaborators.

How it works in ASP.NET Core. At startup you register mappings from a service type to an implementation:

  • builder.Services.AddScoped<IEmployeeService, EmployeeService>()

When a request comes in and the framework needs an EmployeeController, it looks at the constructor, sees it needs an IEmployeeService, creates an EmployeeService (and recursively anything that needs), and injects it. This graph is built per request.

Why it exists. It removes hard-coded new calls, so a class depends on an abstraction rather than a concrete type. That makes code loosely coupled, easy to unit test (inject a fake), and easy to reconfigure (swap the registration).

Where developers use it. Controllers, minimal API handlers, Razor components, other services, middleware (via InvokeAsync parameters), and filters. EF Core's DbContext, IHttpClientFactory, ILogger<T> and IConfiguration are all resolved through the same container.

Terminology. Service — anything registered in the container. Registration — the type-to-implementation mapping. Resolution — the container creating an instance. Lifetime — how long an instance lives (transient, scoped, singleton).

C# — constructor injection
public interface IEmployeeService
{
    Task<IReadOnlyList<Employee>> GetAllAsync();
}

public class EmployeeController : ControllerBase
{
    private readonly IEmployeeService _service;

    // the container supplies IEmployeeService — this class never news it up
    public EmployeeController(IEmployeeService service)
    {
        _service = service;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
        => Ok(await _service.GetAllAsync());
}

// Program.cs
builder.Services.AddScoped<IEmployeeService, EmployeeService>();

Step by step, when a request arrives for an endpoint:

  1. The framework needs to create the EmployeeController. It reads its constructor and sees it requires an IEmployeeService.
  2. It asks the container to resolve IEmployeeService. The container looks up the registration — IEmployeeService → EmployeeService — and prepares to create an EmployeeService.
  3. EmployeeService's own constructor needs an AppDbContext and an ILogger<EmployeeService>, so the container resolves those first (recursively), respecting each one's lifetime.
  4. It constructs the whole graph bottom-up and hands the finished controller to MVC.
  5. At the end of the request, the DI scope is disposed, which disposes every IDisposable scoped and transient service created in it — including the DbContext.

This is why you never call new for a collaborator: the container owns creation, wiring and disposal of the entire object graph.

  • Achieves loose coupling — classes depend on abstractions, not concrete types
  • Makes unit testing easy — inject a mock or stub instead of the real dependency
  • Centralises object creation and wiring in one place (Program.cs)
  • Lets you swap implementations (e.g. a real vs fake payment gateway) by changing one registration
  • Manages object lifetime and disposal for you through service lifetimes

HTTP request GET /api/employees
EmployeeController ctor takes IEmployeeService
EmployeeService ctor takes IEmployeeRepository
EmployeeRepository ctor takes AppDbContext
AppDbContext (EF Core) scoped, one per request
SQL Server SELECT ... FROM Employees

In a real layered application the controller never creates its dependencies. It declares IEmployeeService in its constructor; the service declares IEmployeeRepository; the repository declares AppDbContext. The container builds this whole chain per request, and because AppDbContext is scoped, every layer in one request shares the same context and transaction. In a unit test you construct the controller with a fake IEmployeeService and never touch a database.

Common use cases

  • Injecting a repository or service into every controller
  • Injecting IHttpClientFactory for calls to other services
  • Injecting ILogger<T> and IConfiguration / IOptions<T>
  • Swapping a real email/payment gateway for a fake in tests or staging
  • Registering IEnumerable<IValidator> for a validation pipeline

  • The service locator anti-pattern — injecting IServiceProvider and calling GetService inside methods, which hides the class's real dependencies.
  • Injecting a scoped service (like DbContext) into a singleton — a captive dependency that the container throws on in Development.
  • Newing up a dependency “just this once” instead of registering it, so it can't be replaced in tests.
  • A constructor with 8+ injected services — usually a Single Responsibility violation, not a DI problem.
  • Registering an implementation with the wrong lifetime (transient HttpClient, singleton cache that should be scoped).

Practise the whole thread the way an interviewer walks it — each question builds on the last.

  1. What is Dependency Injection in ASP.NET Core?You are here
  2. What is loose coupling?
  3. What is Inversion of Control (IoC)?
  4. What are service lifetimes in ASP.NET Core?
  5. What is the difference between Transient, Scoped and Singleton?
  6. Why is DbContext registered as Scoped?
  7. What is a captive dependency?