Dependency Injection and Service Lifetimes
LoanService needs a database context, a clock and a logger. If it created them with new, you could never swap the real clock for a fake one in a test, and every class would repeat the setup. ASP.NET Core is built around dependency injection instead: classes ask for what they need, and a container hands it over.
The rule
Ask in the constructor, register once, choose a lifetime: singleton, scoped, transient.
A class declares its dependencies as constructor parameters, usually interfaces:
public class LoanService(LibraryDb db, IClock clock)
{
public async Task<Loan> BorrowAsync(int bookId, int memberId)
{
var loan = new Loan(bookId, memberId, due: clock.Today.AddDays(14));
db.Loans.Add(loan);
await db.SaveChangesAsync();
return loan;
}
}Program.cs registers each service once with the built-in container, Microsoft.Extensions.DependencyInjection:
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddDbContext<LibraryDb>(o => o.UseSqlite("Data Source=library.db"));
builder.Services.AddScoped<LoanService>();When a controller, endpoint or other service asks for LoanService, the container builds it and fills in its constructor — recursively.
The lifetime says how often a new instance is made:
| Lifetime | New instance… | Good for |
|---|---|---|
| Singleton | once for the whole app | stateless helpers, caches, config |
| Scoped | once per HTTP request | DbContext and services that use it |
| Transient | every time it is asked for | small, cheap, stateless objects |
AddDbContext registers the context as scoped.
The shortcut
Apply one rule: a service may only depend on things that live at least as long as it does. Singleton can use singletons. Scoped can use singletons and scoped. Transient can use anything. A singleton that holds a scoped service keeps the first request's instance forever — a captive dependency.
List the lifetimes
IClocksingleton,LibraryDbscoped,LoanServicescoped.
The trap: registering everything as a singleton "for speed". A DbContext is not thread-safe, and sharing one across concurrent requests corrupts its state.
Drills
Answer fast — trust the rule.
How many LibraryDb instances does one HTTP request get when it is registered as scoped?
Which lifetime creates a new instance every time the service is requested?
Which registration creates a captive dependency?
A stateless IClock that just returns the current time is best registered as what?
Remember
- Ask in the constructor, register once, choose a lifetime.
- Depend only on things that live at least as long as you.
- DbContext is scoped; a singleton holding it is a captive dependency.
