How it works
The explanation, with examples you can run.
Lifetimes, observed
#:package Microsoft.Extensions.Hosting@10.0.0using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args);builder.Services.AddSingleton<Clock>();builder.Services.AddScoped<UnitOfWork>();builder.Services.AddTransient<Handler>();using var host = builder.Build(); for (int request = 1; request <= 2; request++){ using var scope = host.Services.CreateScope(); // what ASP.NET Core does per request var h1 = scope.ServiceProvider.GetRequiredService<Handler>(); var h2 = scope.ServiceProvider.GetRequiredService<Handler>(); Console.WriteLine($"request {request}: handlers {h1.Id}/{h2.Id}, unit of work {h1.Uow.Id}/{h2.Uow.Id}, clock {h1.Clock.Id}");}try { host.Services.GetRequiredService<UnitOfWork>(); }catch (InvalidOperationException e) { Console.WriteLine(e.Message); } class Clock { public string Id { get; } = Guid.NewGuid().ToString()[..4]; }class UnitOfWork { public string Id { get; } = Guid.NewGuid().ToString()[..4]; }class Handler(UnitOfWork uow, Clock clock){ public string Id { get; } = Guid.NewGuid().ToString()[..4]; public UnitOfWork Uow => uow; public Clock Clock => clock;}Run with DOTNET_ENVIRONMENT=Development, it printed (ids are random):
request 1: handlers e103/c528, unit of work 8d12/8d12, clock e2efrequest 2: handlers 8709/8b8f, unit of work 371e/371e, clock e2efCannot resolve scoped service 'UnitOfWork' from root provider.Transient handlers differ every time, the scoped unit of work is shared within a request and new for the next one, and the singleton clock never changes. In the default Production environment the last line didn't appear: resolving a scoped service from the root silently returned an instance that lives as long as the app.
Rules that prevent bugs
- A service may depend only on services that live at least as long. Singleton depends on singleton; scoped on scoped or singleton; transient on anything. Breaking this creates a captive dependency: a
DbContextcaptured by a singleton is shared across threads, which it doesn't support. - Background work creates its own scopes. A
BackgroundServiceis a singleton. InjectIServiceScopeFactoryand create a scope per unit of work. - Let the container dispose. Services implementing
IDisposableorIAsyncDisposableare disposed when their scope (or, for singletons, the host) ends. Don't dispose injected services yourself. - Prefer constructor injection. Calling
GetServiceall over the code (the service locator pattern) hides dependencies and defeats validation.
The host
Host.CreateApplicationBuilder (and WebApplication.CreateBuilder for web apps) gives you Configuration layered from appsettings.json, appsettings.{Environment}.json, environment variables and the command line; Logging with console output; and Services. Classes implementing IHostedService are started when the host starts and stopped gracefully on SIGINT or SIGTERM (docker stop sends the latter), and IHostApplicationLifetime lets code react to start-up and shutdown. Bind settings to typed classes with the options pattern (services.Configure<SmtpOptions>(config.GetSection("Smtp")), then inject IOptions<SmtpOptions>).
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.