DI is the backbone of every ASP.NET Core app: databases, HTTP clients, loggers and your own services flow through it.
ASP.NET Core has a built-in dependency injection container. You register services on builder.Services, typically as an interface plus implementation, and the framework constructs them and passes them wherever they're needed: endpoint handler parameters, controller constructors, middleware, other services.
Lifetimes:
- Singleton (
AddSingleton): one instance for the whole app. Must be thread-safe. Good for caches, configuration wrappers,HttpClientfactories. - Scoped (
AddScoped): one instance per HTTP request. The default for EF CoreDbContextand unit-of-work style services. - Transient (
AddTransient): a new instance every time it's requested. For lightweight, stateless services.
The container resolves constructor parameters recursively, so a service can depend on other services. Prefer constructor injection; avoid calling IServiceProvider.GetService yourself (the service locator anti-pattern).
The classic bug is a captive dependency: a singleton that takes a scoped service in its constructor keeps one instance of it forever, sharing a DbContext across requests. In Development the container validates scopes and throws on startup for this.
Other features: TryAdd* to avoid duplicate registrations, keyed services (AddKeyedSingleton<IStore, S3Store>("s3") with [FromKeyedServices("s3")]), and open generics (AddScoped(typeof(IRepo<>), typeof(Repo<>))). Libraries expose AddX() extension methods that register their services in one call.
Example
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddScoped<RequestCounter>();
builder.Services.AddTransient<IGreeter, Greeter>();
var app = builder.Build();
app.MapGet("/hello/{name}", (string name, IGreeter greeter, RequestCounter c1, RequestCounter c2) =>
{
c1.Hits++; c2.Hits++; // same scoped instance within a request
return $"{greeter.Greet(name)} (hits this request: {c1.Hits})";
});
app.Run();
// GET /hello/Ada (before noon) -> Good morning, Ada (hits this request: 2)
interface IClock { DateTime Now { get; } }
class SystemClock : IClock { public DateTime Now => DateTime.Now; }
class RequestCounter { public int Hits { get; set; } }
interface IGreeter { string Greet(string name); }
class Greeter(IClock clock) : IGreeter // constructor injection
{
public string Greet(string name) =>
$"{(clock.Now.Hour < 12 ? "Good morning" : "Hello")}, {name}";
}Remember
- Register on builder.Services; the container builds and injects instances
- Singleton = app, Scoped = request, Transient = every resolve
- Never inject scoped services into singletons (captive dependency)
- Prefer constructor injection over resolving from IServiceProvider
Watch out for
- Registering a DbContext-using service as a singleton, sharing one non-thread-safe context across requests.