Dependency injection and service lifetimes
Transient, scoped and singleton, what a scope is in ASP.NET Core, and who disposes what the container creates.
After this lesson you can
- Define transient, scoped and singleton precisely
- Predict how many instances a sequence of resolutions creates
- Explain when the container disposes a service and when it does not
- Say which registration a constructor gets when there are several
ASP.NET Core ships a dependency injection container. You describe services at startup; the container builds them, and their dependencies, when something asks.
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddTransient<IPriceCalculator, PriceCalculator>();
The lifetime decides how often a new instance is made:
- Transient: a new instance every time the service is resolved, so two constructors asking for it in the same request get two objects.
- Scoped: one instance per scope. ASP.NET Core opens a scope for each
request and disposes it when the request ends, so "scoped" means "one
per request" in a web app.
AddDbContextregisters the context as scoped. - Singleton: one instance for the root provider, created the first time it is needed (or the instance you passed in) and kept for the life of the app.
The example is a model, not the real container: twenty lines that apply the same three rules, and two requests that each resolve every service twice.
public class Container{ readonly Dictionary<string, string> lifetimes = new(); readonly Dictionary<string, string> singletons = new(); int counter; public void Register(string service, string lifetime) => lifetimes[service] = lifetime; public string Resolve(string service, Dictionary<string, string> scope) { switch (lifetimes[service]) { case "Singleton": if (!singletons.TryGetValue(service, out var s)) singletons[service] = s = New(service); return s; case "Scoped": if (!scope.TryGetValue(service, out var sc)) scope[service] = sc = New(service); return sc; default: return New(service); } } string New(string service) => service + "#" + ++counter;} public static class Solution{ public static List<string> Run() { var c = new Container(); c.Register("Clock", "Singleton"); c.Register("Db", "Scoped"); c.Register("Validator", "Transient"); var lines = new List<string>(); for (int request = 1; request <= 2; request++) { var scope = new Dictionary<string, string>(); // one scope per request var got = new List<string>(); foreach (var service in new[] { "Clock", "Clock", "Db", "Db", "Validator", "Validator" }) got.Add(c.Resolve(service, scope)); lines.Add($"request {request}: " + string.Join(" ", got)); } return lines; }}Disposal
The container disposes every IDisposable it created, when the scope
that owns it is disposed: scoped and transient services at the end of the
request, singletons when the app shuts down. An object you constructed
yourself and registered with AddSingleton(instance) is yours to
dispose. A disposable transient resolved from the root provider is held
by the root until shutdown, so resolving one repeatedly there grows
memory for the life of the process.
Several registrations for one service
Registering a service twice keeps both registrations. A constructor that
takes IPriceCalculator gets the last one registered; a constructor
that takes IEnumerable<IPriceCalculator> gets all of them in
registration order. TryAddSingleton and friends only register when the
service has no registration yet, which is how libraries supply defaults
that the application can replace.
Common mistakes
- Registering something with per-user or per-request state as a
singleton. Every concurrent request then shares one object; a
DbContextin particular is not safe to use from two threads at once. - Assuming transient means "per request". Two services in the same
request that both depend on a transient get two separate instances,
like
Validator#3andValidator#4in the model. - Resolving a scoped service from
app.Servicesat startup. With scope validation on, which is the default in the Development environment, this throws; with it off, the instance silently lives as long as the app. - Calling
builder.Services.BuildServiceProvider()yourself to fetch a service during startup. That builds a second container with its own singletons, separate from the one the app uses.
Try it yourself
2 visible tests · 3 hidden testsModel the container. registrations holds entries "Name:Lifetime"
with lifetime Transient, Scoped or Singleton; if a name is
registered more than once, the last entry's lifetime is the one used.
resolutions holds entries "scope:Name", resolved in order. Scopes
are identified by any string; the scope root stands for the root
provider, and a scoped service resolved from the root is cached in the
root exactly like any other scope.
Implement Labels(List<string> registrations, List<string> resolutions)
returning one label per resolution, in order, shaped "Name#k", where
k counts instances of that service in creation order starting at 1.
Every name resolved is registered.
Labels(["Clock:Singleton","Db:Scoped","Mapper:Transient"], ["r1:Clock","r1:Db","r1:Db","r1:Mapper","r2:Clock","r2:Db","r2:Mapper"])Labels(["Db:Scoped"], ["a:Db","b:Db","a:Db","b:Db","c:Db"])
Sign up to run the tests, check the hidden ones and save your progress. Sign up