DEV Community

FakeStandard
FakeStandard

Posted on

Exploring .NET Dependency Injection Lifetimes with Real Examples

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)