When we register services in the DI (Dependency Injection) container in .NET, the service instance has an important concept called Lifetime, it tells the system when the service is created, shared, and disposed.
📌 What is Service Lifetime
.NET provides three types of service lifetimes
- Singleton
- Scoped
- Transient
Here's a quick look at what they mean behind the scenes
namespace Microsoft.Extensions.DependencyInjection
{
public enum ServiceLifetime
{
Singleton = 0, // One instance for the entire app lifetime
Scoped = 1, // One instance per scope (typically per request)
Transient = 2 // A new instance every time it's requested
}
}
Singleton
The instance is created on the first request, and then it just sticks around. No matter how many requests come in after that, it will keep using that one instance throughout the app's lifetime.
Scoped
In a web app, a new instance is created per client or per request scoped. So inside the same browser session, you get the same instance. But across different clients or browsers, each gets its own instance.
Transient
A new instance is created every time it's requested. Once the request ends, that instance is gone.
🍵 How to Test
Since testing in a Console App can only really show the difference between Singleton and Transient, we'll use a Web App to demonstrate all three lifetimes.
We first create the Operation
class and interface, and define interfaces that represent the three different lifetimes, all inheriting from IOperation
.
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public string OperationId { get; }
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
}
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
Next, register the services with different lifetimes in Program.cs
.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// Add services to the container with lifetime
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
Inject the three service instances into HomeController.cs
.
public class HomeController(ILogger<HomeController> logger,
IOperationSingleton singleton,
IOperationScoped scoped,
IOperationTransient transient) : Controller
{
private readonly ILogger<HomeController> _logger = logger;
private readonly IOperationSingleton _singleton = singleton;
private readonly IOperationScoped _scoped = scoped;
private readonly IOperationTransient _transient = transient;
// ...
}
In the Index
action, get the OperationId
of each instance and store them in the ViewBag
.
public IActionResult Index()
{
ViewBag.Singleton = _singleton.OperationId;
ViewBag.Scoped = _scoped.OperationId;
ViewBag.Transient = _transient.OperationId;
return View();
}
Then in index.cshtml
, retrieve the values from the ViewBag
.
<table class="table">
<thead>
<tr>
<td>Service Lifetime</td>
<td>OperationId</td>
</tr>
</thead>
<tbody>
<tr>
<td>Singleton</td>
<td>@ViewBag.Singleton</td>
</tr>
<tr>
<td>Scoped</td>
<td>@ViewBag.Scoped</td>
</tr>
<tr>
<td>Transient</td>
<td>@ViewBag.Transient</td>
</tr>
</tbody>
</table>
Finally, run the app, open it in three different browsers, and observe the differences in the OperationId
values.
In all three browsers, the OperationId
for the Singleton service remains the same. Refreshing the page or opening a new browser window doesn't change it. This confirms that the Singleton instance is created during the first request and reused throughout the app's lifetime.
At this point, there's no visible difference between Scoped and Transient. To help observe their differences, we modify the code to inject two instances of each service.
public class HomeController(ILogger<HomeController> logger,
IOperationSingleton singleton,
IOperationScoped scoped,
IOperationTransient transient,
IOperationSingleton singleton2, // second instance
IOperationScoped scoped2, // second instance
IOperationTransient transient2 // second instance
) : Controller
{
private readonly ILogger<HomeController> _logger = logger;
private readonly IOperationSingleton _singleton = singleton;
private readonly IOperationScoped _scoped = scoped;
private readonly IOperationTransient _transient = transient;
private readonly IOperationSingleton _singleton2 = singleton2;
private readonly IOperationScoped _scoped2 = scoped2;
private readonly IOperationTransient _transient2 = transient2;
public IActionResult Index()
{
ViewBag.Singleton = _singleton.OperationId;
ViewBag.Scoped = _scoped.OperationId;
ViewBag.Transient = _transient.OperationId;
ViewBag.Singleton2 = _singleton2.OperationId;
ViewBag.Scoped2 = _scoped2.OperationId;
ViewBag.Transient2 = _transient2.OperationId;
return View();
}
}
And modify tbody
tag.
<tbody>
<tr>
<td>Singleton</td>
<td>@ViewBag.Singleton</td>
<td>@ViewBag.Singleton2</td>
</tr>
<tr>
<td>Scoped</td>
<td>@ViewBag.Scoped</td>
<td>@ViewBag.Scoped2</td>
</tr>
<tr>
<td>Transient</td>
<td>@ViewBag.Transient</td>
<td>@ViewBag.Transient2</td>
</tr>
</tbody>
Now, we can observe that the OperationId
of Transient services is always different—meaning a new instance is created on every request and disposed of afterwards.
Meanwhile, the Scoped service has the same OperationId
within the same browser session, but different values in different browsers. This confirms that each client request uses a different instance, but within a single client, the same instance is reused across multiple requests.
💡 Key points
- Singleton — stays the same instance everywhere.
- Scoped — has the same instance within the same browser (client), no matter how many times you call it or inject it. But across different browsers (clients), it's different. So it shares the instance per client/request.
-
Transient — gives you a different
OperationId
every single time, even when injected twice in the same request. it's always a new instance, and it's disposed of once the request is done.
🔎 When to use each lifetime?
Singleton
Use it for — Stateless, heavy-to-create services, global settings, caching, or long-running resources.
Examples: Configuration, Logger, Memory Cache, Queue, Timer.
⚠️ Don't depend on Scoped or Transient services inside a Singleton, it may cause weird bugs.
Scoped
Use it for — Sharing data during a request, DB connection scoped, or objects tied to business logic.
Examples: Repositories, Unit of Work, session-based cache, request-level state tracking.
⚠️ Avoid injecting Singleton services into Scoped ones.
Transient
Use it for — Lightweight, stateless services that don't need to share data.
Examples: Validators, Calculators, Formatters, Utility Classes.
⚠️ Overusing Transient can lead to too many objects and unnecessary GC pressure.
🍺 Wrapping Up
Now that you understand how service lifetimes work, you'll have a much better sense of how to register services properly and manage resources effectively.
Easy, right? Job done ☑️
Thanks for reading!
If you like this article, please don't hesitate to click the heart button ❤️
or follow my GitHub I'd appreciate it.
Top comments (0)