Dependency Injection in ASP.NET Core: constructor injection, composition root, keyed services

.NET Architecture & Design Patterns November 13, 2026 csharp

A class receives its dependencies from outside (usually through the constructor) instead of creating them with new.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ======================================================================
// Q: What is Dependency Injection and why do we use it?
//
// A class receives its dependencies from outside (usually through the
// constructor) instead of creating them with new. It is how Inversion of Control
// is implemented. ASP.NET Core has a built-in container
// (Microsoft.Extensions.DependencyInjection). Benefits: loose coupling,
// testability (fakes / mocks), all the wiring in one place (composition root =
// Program.cs), and the container manages lifetimes (see Q5).
// - Service Locator (IServiceProvider.GetService inside business logic) is an
//   anti-pattern: it hides dependencies.
// - A constructor with 7-8+ dependencies signals that the class does too much
//   (an SRP violation).
// - Use TimeProvider (.NET 8) instead of DateTime.UtcNow, so tests can use
//   FakeTimeProvider.
// - DI is not DIP: DI is the mechanism, DIP (Q18) is the principle.
// ======================================================================

// WRONG: tight coupling, cannot be tested without a real ERP
public class OrderService
{
    private readonly ErpClient _erp = new ErpClient(new HttpClient());
}

// RIGHT: constructor injection (primary constructor, C# 12)
public class OrderService(IOrderRepository orders, IErpClient erp, TimeProvider clock) : IOrderService
{
    public async Task SubmitAsync(int orderId, CancellationToken ct) { /* ... */ }
}

// Composition root: Program.cs
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddSingleton(TimeProvider.System);                              // testable time
builder.Services.Configure<ErpOptions>(builder.Configuration.GetSection("Erp")); // Options pattern
builder.Services.AddKeyedScoped<IShippingCalculator, DhlCalculator>("dhl");      // keyed services (.NET 8)