Skip to Content
Course content

237: Service Lifetimes: Transient, Scoped, Singleton

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been working with Dependency Injection (DI) in .NET, you've probably noticed that when you register a service in Program.cs, you have to pick one of three methods: AddTransient, AddScoped, or AddSingleton. If you're just starting out, they might feel interchangeable, but picking the wrong one can lead to some truly bizarre bugs—like data leaking between users or memory leaks that crash your server.

To wrap your head around this, imagine you're running a coffee shop. Think about the things your staff interacts with throughout the day:

  • The Disposable Cup (Transient): Every time a customer orders a drink, the barista grabs a brand new paper cup. Once that drink is gone, the cup is tossed. You never reuse a cup for a second order.
  • The Cafe Table (Scoped): When a customer walks in, they take a table. For the duration of their "visit" (their request), they use that same table for their coffee, their muffin, and their laptop. But once they leave, that table is cleared and is no longer associated with them.
  • The Espresso Machine (Singleton): There is only one high-end espresso machine in the whole shop. Every single customer, every single barista, and every single order uses that one specific machine from the moment the shop opens until it closes at night.

Fresh Every Time: Transient

In C#, a Transient service is exactly like that paper cup. The DI container creates a brand new instance of the service every single time it's requested. If you inject a transient service into three different classes in the same HTTP request, you'll get three different objects.

builder.Services.AddTransient<IGuidGenerator, GuidGenerator>();

I usually use Transient services for lightweight, stateless components. If the class doesn't hold onto any data and just performs a quick calculation or a transformation, Transient is the safest bet because you don't have to worry about multi-threading issues or "stale" data.

One Per Request: Scoped

A Scoped service is the "table" of your application. In a web app, a "scope" is typically a single HTTP request. The first time a service is requested during a web request, .NET creates it. Every other class that asks for that same service within that same request gets the exact same instance.

builder.Services.AddScoped<IUserSession, UserSession>();

This is incredibly powerful for things like database contexts (DbContext) or user session data. You want your Repository and your Service layer to share the same database connection during a single request so you can use a single transaction. If you used Transient here, you'd be opening and closing database connections constantly, which would kill your performance.

The Single Source of Truth: Singleton

A Singleton is the espresso machine. It is created the first time it's requested (or when the app starts) and it lives until the process terminates. Every single user and every single request shares this one instance.

builder.Services.AddSingleton<ICacheService, CacheService>();

Singletons are great for configuration settings or in-memory caches. However, be careful. Because everyone shares the same instance, you have to make sure the class is thread-safe. If two different users try to update a list inside a Singleton at the exact same millisecond, you're going to have a bad time unless you've implemented proper locking.

The Captive Dependency Trap

Here is a pro tip that will save you hours of debugging: Never inject a Scoped service into a Singleton service.

I call this the "Captive Dependency." If a Singleton (which lives forever) takes a Scoped service (which is supposed to die after the request) in its constructor, that Scoped service is now "captured." It will be held in memory for the life of the application, effectively turning it into a Singleton. This often leads to database connection crashes because the DbContext was never disposed of.




📋 Practical Task

Build a Lifetime Correlation Tracker

To see these lifetimes in action, you're going to build a small diagnostic tool. Create a simple ASP.NET Core Web API project and implement the following:

  1. Create an interface ILifetimeId with a single property: Guid Id { get; }.
  2. Create three separate classes that implement this interface: TransientId, ScopedId, and SingletonId. Each should assign a Guid.NewGuid() to the Id property in its constructor.
  3. Register these three classes in Program.cs using their respective lifetimes (AddTransient, AddScoped, and AddSingleton).
  4. Create a service called LifetimeDemoService that takes all three ILifetimeId implementations in its constructor. Give it a method that returns the IDs as a string.
  5. Create a Controller with an endpoint that injects both the LifetimeDemoService and the three ILifetimeId services directly.
  6. The endpoint should return a JSON object showing the IDs from the service AND the IDs injected directly into the controller.

The Goal: Refresh your browser page several times. Observe which IDs change on every refresh, which IDs stay the same within one request but change on the next, and which ID never changes no matter how many times you refresh or restart the request (but changes when you restart the app).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.