Lesson 9 of 45 intermediate

Dependency Injection and Service Lifetimes

Who creates your objects, and how long they live

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

Two different classes handle the same HTTP request and both inject a Scoped service. How many instances are created?

Open interactive version (quiz + challenge)

Real-world analogy

Think of a restaurant. The salt shaker on each table is brought fresh for every guest who asks (Transient). The waiter assigned to your table stays with you for the whole meal, then leaves (Scoped). The kitchen is one kitchen for everyone, all night (Singleton). Here the meal is one HTTP request, and the manager who decides all this is the DI container.

What is it?

Dependency Injection (DI) means a class receives the objects it needs from outside instead of creating them. ASP.NET Core has a built-in container: you register an interface with an implementation and a lifetime, and the framework hands the right instance to every constructor that asks. The lifetime — Transient, Scoped or Singleton — decides whether the container reuses an instance or builds a new one.

Real-world relevance

In a real API you register the DbContext and your repositories as Scoped, stateless helpers as Transient, and things like a cache client or configuration as Singleton. The classic production bug is a Singleton that injects the DbContext directly. Parallel requests then share one context and throw "A second operation was started on this context". The Development startup check catches it, but only when you run in Development.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

What goes wrong when a Singleton service takes a DbContext in its constructor?

Which lifetime is the default for a DbContext registered with AddDbContext?

Code example

var builder = WebApplication.CreateBuilder(args);

// One line per service: interface, implementation, lifetime
builder.Services.AddTransient<IPriceCalculator, PriceCalculator>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddSingleton<IClock, SystemClock>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

app.MapGet("/orders/{id}", async (int id, OrderService service) =>
    await service.GetTotalAsync(id));

app.Run();

public class OrderService
{
    private readonly IOrderRepository _orders;
    private readonly IPriceCalculator _prices;

    // The container sees this constructor and supplies both
    public OrderService(IOrderRepository orders, IPriceCalculator prices)
    {
        _orders = orders;
        _prices = prices;
    }

    public async Task<decimal> GetTotalAsync(int id)
    {
        var order = await _orders.FindAsync(id);
        return _prices.Total(order);
    }
}

Line-by-line walkthrough

  1. 1. The three Add... lines tell the container: when someone asks for this interface, build this class, and reuse it this long.
  2. 2. AddTransient gives a new PriceCalculator on every request for one. AddScoped gives one SqlOrderRepository per HTTP request. AddSingleton gives one SystemClock for the whole app.
  3. 3. In MapGet the parameter OrderService is not created by you. The container builds it for this request.
  4. 4. The OrderService constructor only names interfaces. It does not know or care which concrete classes arrive.
  5. 5. GetTotalAsync just uses the two helpers. In a unit test you pass fakes into the same constructor.

Spot the bug

public class ReportWorker : BackgroundService
{
    private readonly AppDbContext _db;

    public ReportWorker(AppDbContext db) => _db = db;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var count = await _db.Orders.CountAsync(ct);
            await Task.Delay(TimeSpan.FromMinutes(5), ct);
        }
    }
}
Need a hint?
A BackgroundService is registered as a Singleton. What lifetime does AppDbContext have?
Show answer
A Singleton is holding a Scoped DbContext (captive dependency). The context is never disposed and keeps stale tracked data. Fix: inject IServiceScopeFactory, and inside the loop create a scope with CreateScope(), get AppDbContext from scope.ServiceProvider, and dispose the scope each round.

Explain like I'm 5

Instead of every worker buying their own tools, a manager hands each worker the tools they need. Some tools are new each time, some are shared for one job, and some are shared by the whole building.

Fun fact

In the Development environment ASP.NET Core checks your registrations at startup and throws if a Singleton depends on a Scoped service. In Production that check is off by default, so the bug only shows up under real traffic.

Hands-on challenge

Say it out loud in 30 seconds: "What is the difference between Transient, Scoped and Singleton, and what breaks if you inject a Scoped service into a Singleton?" Then check: did you mention one-per-request, the DbContext example, and the captive dependency?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook