An instance for which unit
Transient creates an instance per resolution; scoped retains an instance within its scope; singleton shares an instance in the container. In the HTTP APIs considered here, a request has a scope. Two consumers resolving the same scoped registration within that request receive the same instance, provided no additional scopes are created.
The scope is not a thread: asynchronous processing can continue on another thread. Nor is it a universal rule for user interfaces with persistent circuits. RequestStamp only demonstrates identity within a request. A singleton with shared mutable state needs its own concurrency analysis.
Work outside the request
A BackgroundService is registered as singleton and does not automatically receive a scope per job. To use a scoped repository, it can receive IServiceScopeFactory, create a scope per unit, resolve the repository there and dispose the scope when the unit finishes. The operation must finish before its dependencies are disposed.
Resolving a scoped service from the root provider with validation disabled can extend its life until container shutdown. Disabling validation does not correct that boundary. If dependencies require asynchronous disposal, the design must also respect that contract; retaining the provider of a completed request is insufficient.
Configuration has precedence
With CreateBuilder(args) and default providers, command-line arguments have priority over environment variables, which have priority over application JSON files. The environment-specific file overrides appsettings.json. In Development, user secrets also enter the default composition below environment variables.
For Delivery:BatchSize, JSON=20, environment=25 and argument=60 produce 60. Changing only JSON does not correct that value. The conclusion depends on the declared composition: adding a provider last can change priority. Do not turn default ordering into an absolute rule for any customized host.
Hierarchical names and prefixes
In the environment provider, Delivery__BatchSize represents Delivery:BatchSize. A double underscore becomes the hierarchy separator and avoids depending on system support for colons in variable names. A single underscore or a dot does not express the same transformation.
AddEnvironmentVariables(prefix: "OPS_") filters variables by prefix and removes it from the resulting key. If added after CreateBuilder, OPS_Delivery__BatchSize=12 can override default argument 25. The prefix bounds the source; it does not necessarily create an OPS section or reduce provider priority.
Read, retain and update options
IOptions provides a view without post-startup reload. IOptionsSnapshot is scoped: it computes and retains options within the scope, allowing new reads in later requests when the source supports reload. IOptionsMonitor is singleton and provides current values and change notifications. Choose according to the consumer’s needs and lifetime.
A source without reload support does not gain it merely by changing the interface. Copying CurrentValue fields once into a private object does not automatically connect that object to changes. Define the consumer’s reading or updating behavior, including consistency for an operation using several values.
Accept configuration at the right time
Bind associates values with an options class; it does not establish their acceptability. In the example, Range(1,100) declares the interval, ValidateDataAnnotations activates validation of those annotations and ValidateOnStart requires evaluation at startup. Without that last step, validation can wait until first options access.
BatchSize=0 violates the contract even though it is a convertible integer. The initial zero also makes omission invalid in this example. Valid configuration establishes neither actual delivery capacity, database access nor readiness of every dependency. Those are additional criteria assessed at their own boundaries.
Compare the model with evidence
Read Program.cs with Delivery__BatchSize=25 and no other winning source. The endpoint receives scoped options and a scoped RequestStamp, returning only the non-secret value and illustrative identifier. It is not an endpoint for publishing all application configuration. The same request retains the instance; different requests should not share that scoped object.
Now consider two separate incidents: an old argument keeps BatchSize=60 despite environment value 25; a singleton worker captures a scoped repository. The first requires correcting the effective source. The second requires reviewing the unit-of-work boundary. Questions assess these decisions without executing the example.
// Program.cs: ASP.NET Core 10.0 Web SDK, net10.0, ImplicitUsings enabled.
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<RequestStamp>();
builder.Services.AddOptions<DeliveryOptions>()
.Bind(builder.Configuration.GetSection("Delivery"))
.ValidateDataAnnotations()
.ValidateOnStart();
var app = builder.Build();
app.MapGet("/configuration-preview",
(IOptionsSnapshot<DeliveryOptions> options, RequestStamp stamp) =>
Results.Ok(new { batchSize = options.Value.BatchSize, request = stamp.Id }));
app.Run();
public sealed class DeliveryOptions
{
[Range(1, 100)]
public int BatchSize { get; set; }
}
public sealed class RequestStamp
{
public string Id { get; } = Guid.NewGuid().ToString("N");
}
Assume Delivery__BatchSize=25 and no arguments or later providers for that key. The value passes Range(1,100). Zero, including the initial value when configuration is missing, is rejected at startup.
The endpoint only shows a non-secret value and an illustrative identifier; it delivers no orders.
Common pitfalls
Capturing a scoped service in a singleton; resolving scoped services at the root; assuming Range executes merely because it exists; treating IOptionsSnapshot as singleton; confusing notification with automatic updating of private copies.
Related topics: Hosting, middleware and request flow · Persistence and concurrency · Health, observability and diagnosis
Declare the lifetime unit and value source. Then decide when to read, update and reject invalid configuration. Registration, binding and validation address different responsibilities that must be connected explicitly.
Reference: Dependency lifetime behavior · ASP.NET Core 10.0 / .NET 10 / C# 14