Imagine your ASP.NET Core application is a well-run restaurant.
Each HTTP request is like a new customer walking in. The services in your app? They're your restaurant staff — chefs, waiters, assistants.
Now, you’re the manager. When you hire someone (register a service), you must decide:
Do I need this person once for the whole restaurant? Or one per customer? Or one per order?
That’s exactly what Singleton, Scoped, and Transient lifetimes help you define.
1. Singleton – One Chef for the Whole Restaurant
🍴 Restaurant Analogy:
You're running a kitchen with one Master Chef. Every time a customer walks in — whether it's breakfast, lunch, or dinner — this same chef prepares all dishes.
No matter how many customers or orders, there’s only one of him. He stays there from restaurant open till close.
🧠 What It Means in Code:
In .NET, this means:
The same instance of a service is reused across all requests, throughout the lifetime of the application.
It's created once and shared — no matter how many controllers or services ask for it.
services.AddSingleton<IChefService, ChefService>();
Real Behavior:
Every time you inject IChefService
, you get the same object, same memory reference, same state.
https://medium.com/@dotnetfullstackdev
Example:
public interface IChefService
{
Guid GetChefId();
}
public class ChefService : IChefService
{
private Guid _chefId = Guid.NewGuid();
public Guid GetChefId() => _chefId;
}
Injected in multiple controllers:
public class OrdersController : Controller
{
private readonly IChefService _chefService;
public OrdersController(IChefService chefService)
{
_chefService = chefService;
}
public IActionResult Cook() =>
Ok($"Cooking by chef: {_chefService.GetChefId()}");
}
Output every time:
“Cooking by chef: 1234-abcd
”
Same Guid
, same instance across the app.
2. Scoped – One Waiter Per Table (One Per Request)
🍴 Restaurant Analogy:
A new customer walks into your restaurant and is assigned a dedicated waiter. This waiter is with them for the whole meal — takes their order, brings water, serves food, and gives the bill.
But when the next customer walks in, they get a different waiter.
🧠 What It Means in Code:
A new instance is created for each HTTP request.
Every service within that same request gets the same instance.
Across different requests, the instance changes.
services.AddScoped<IWaiterService, WaiterService>();
Example:
public interface IWaiterService
{
Guid GetWaiterId();
}
public class WaiterService : IWaiterService
{
private Guid _id = Guid.NewGuid();
public Guid GetWaiterId() => _id;
}
Two API calls, like /order/details
and /order/summary
, within the same request pipeline will get the same waiter.
But if two different users make requests, they'll each have their own waiter (instance).
🧠 Use when you want to scope logic to one customer/session/request, like
DbContext
.
3. Transient – A New Busser for Every Task
🍴 Restaurant Analogy:
Every time someone at a table needs something small — say a glass of water, a napkin, or the dessert menu — you assign a new assistant.
Each job, no matter how small, gets a fresh helper. They appear, do the task, and vanish.
🧠 What It Means in Code:
A brand-new instance is created every time the service is requested.
Multiple injections in the same request will result in multiple objects.
services.AddTransient<IAssistantService, AssistantService>();
Example:
public interface IAssistantService
{
Guid GetId();
}
public class AssistantService : IAssistantService
{
private Guid _id = Guid.NewGuid();
public Guid GetId() => _id;
}
Call this service multiple times even in the same controller, and each call gives you a new ID.
public IActionResult Serve()
{
var a1 = _assistant1.GetId();
var a2 = _assistant2.GetId();
return Ok($"{a1} | {a2}"); // Two different GUIDs
}
Perfect for stateless utilities, loggers, mappers, and lightweight tasks.
A Warning from the Kitchen: Mixing Lifetimes
Be careful when injecting a short-lived service (Scoped or Transient) into a long-lived one (Singleton).
It’s like having a per-table waiter stored in a global chef — but the table might be long gone.
This causes:
Memory leaks
Unexpected stale data
Thread-safety issues
To fix this: inject an IServiceProvider
, or use factory/delegate patterns.
Final Words
Think beyond syntax.
Choosing the right lifetime is architectural design:
Singleton gives consistency and resource sharing
Scoped provides request-specific control
Transient favors quick, disposable operations
If you treat your services like restaurant staff, you’ll always know who to hire, when, and why.