Dependency injection

ASP.NET Core · Essentials · 14 min

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, HttpClient factories.
  • Scoped (AddScoped): one instance per HTTP request. The default for EF Core DbContext and 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.

Check yourself

  1. A singleton service takes a scoped service in its constructor. What's the problem?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection