The foundation: what the official documentation explains
ASP.NET Core has a built-in service container. Transient services are created for each resolution, scoped services are shared within a scope, and singleton services live for the container lifetime. Web requests normally provide their own scopes. A dependency should not be captured by an object that outlives it; a singleton holding a request-scoped service is a common lifetime mismatch.
Implementation context: The examples use a .NET 10 MVC application organized into feature folders (Vertical Slice Architecture). Basic C# classes and async/await are assumed. Reference excerpts show selected parts of that application; separately labeled teaching adaptations explain alternatives. They are not complete standalone projects.
Begin with the dependency, not the interface
The Country create handler needs database access. Its constructor asks for AppDbContext; it does not read a connection string or create a new context. This makes the dependency visible at the point where a caller creates the handler. An interface can be useful when multiple implementations or a boundary require it, but introducing one is not necessary merely to demonstrate constructor injection.
Reference excerpt: Areas/Admin/Country/Cqrs/CreateCountryHandler.cs
private readonly AppDbContext _context;
public CreateCountryHandler(AppDbContext context)
{
_context = context;
}
See where the database context comes from
Database infrastructure registers AppDbContext with AddDbContext and chooses a provider from configuration. In the SQL Server branch, the registration supplies both a connection string and command timeout. At request time, an endpoint parameter of this registered service type is resolved through the container.
The excerpt below is one branch of the registration method, so it depends on the surrounding dbSettings binding. It is not intended to replace a whole Program.cs file. Notice that the handler does not know whether the database registration used SQL Server or PostgreSQL.
Reference excerpt: Infrastructures/Databases/DI.cs
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
dbSettings.MsSQL.ConnectionString,
sqlOptions =>
{
sqlOptions.CommandTimeout(dbSettings.MsSQL.TimeoutInSeconds);
}
)
);
}
Why new Handler(db) still uses dependency injection
The Country endpoint receives AppDbContext db from ASP.NET Core, then calls new CreateCountryHandler(db). The handler receives its dependency from the outside. However, the handler itself is not resolved by the container. That distinction matters when adding constructor parameters: the endpoint must pass them explicitly.
This direct composition keeps a small handler easy to follow. A container-managed alternative would register the handler and request it as an endpoint parameter. Both approaches can be valid. The following adaptation illustrates the alternative; it is not a claim about the reference project registration.
Alternative composition: replace the existing POST mapping, do not add a duplicate
// Before builder.Build():
builder.Services.AddScoped<CreateCountryHandler>();
// In the already-authorized Country route group:
group.MapPost("/", async (CreateCountryRequest request,
CreateCountryHandler handler, CancellationToken ct) =>
{
var result = await handler.HandleAsync(request, ct);
return result.Success
? Results.Created($"/api/country/{result.Data?.Id}", result)
: Results.BadRequest(result);
});
Choose lifetime by ownership
For a request that writes one country, keep database work within the request scope and await each operation. Do not save the context in a static field, store it in browser session state, or reuse it from a later background job. A background job needs its own appropriately managed scope or context rather than the instance from the request that scheduled it.
Making the handler a singleton because it appears stateless is a trap if it holds AppDbContext. The field retains the dependency even when the handler has no other mutable properties. A scoped handler is an understandable default for this particular container-managed alternative. A transient handler can also consume the scoped context while resolved inside the request.
Diagnose an unresolved service systematically
Read the full exception to identify the missing type and the type being activated. Then find the registration and confirm that execution reaches it before Build. In this project, choosing no database provider leaves the registration method without an AppDbContext registration. That configuration failure can later present as a dependency-resolution error.
For a learning exercise, inspect two resolutions within one request scope and another in a separate scope. The scoped context should be shared in the first scope and differ in the second. Do not run parallel EF operations on that shared instance to test concurrency: scope sharing is not a guarantee of thread safety.
Key Takeaways
- Constructor injection does not require container-managed handler creation.
- Keep AppDbContext ownership aligned with the work being performed.
- Check conditional registrations when a required service cannot be resolved.
FAQ
Must every handler be registered in DI?
No. The reference endpoints construct their handlers directly and pass the already-resolved context.
Can a singleton hold AppDbContext?
A request-scoped context should not be captured by a singleton. Create an appropriate scope for separate background work.
Does a scoped service mean one instance per user?
No. In a typical MVC app it means one instance per request scope, not per login session.