Dependency Injection service lifetimes in .NET

In this post, I am going to explain how to configure the DI service lifetimes in .NET.

When working with services in .NET Core Web Application you must be familiar with how to register these services to use them with DI (Dependency Injection). The DI is a software design pattern or technique for achieving the Inversion of control principle (IoC).

The IoC principle says that your application should rely on and work with abstractions, not implementations. Your high-level modules should use low-level modules through abstraction, reducing application dependencies and making your product easy to scale.

Using IoC is all about creating instances – when to create these instances, and what is their scope? Here comes the AddTransient, AddScoped, and AddSingleton helper methods. They are used to configure the DI service lifetimes in .NET. The difference between these methods is the scope (the lifetime) of the created instances:

AddTransient

Services registered as transient are always different. Each time a service is needed – a new instance is created. They are different between requests and between objects, which depends on them.

When to use AddTransient

  • Register as transient lightweight and stateless services, which do not cost too much for creation, and you donโ€™t care about the state between requests.
  • Avoid it when you use a service multiple times for making database queries within the same request because a new instance with a database connection will be created each time. In this scenario, you should consider using AddScoped.

AddScoped

The instances are the same within the same request, for each object which depends on the service. When a new request comes โ€“ a new instance is created, but still โ€“ it is the same for every dependent object.

When to use AddScoped

  • Can be efficient when multiple queries are created to a DB, using the same service, within the same request. This way .NET will create only one instance and will reuse it for each query.

AddSingleton

The same for every request and for every object, which depends on them. The instance is created the first time the object is requested and disposed of on application shutdown.

When to use AddSingleton

  • Usable for caching or application configuration-related services, as they make sense to be the same during the whole program lifetime.

Example

Lets look at the following example:

Firstly, we have a simple .NET Core MVC Application, with create services, to represent the difference between AddTransient, AddScoped and AddSingleton methods. Secondly, let’s create 3 interfaces โ€“ ItransientService, IScopedService and ISingletonService:

C#
namespace DIServiceLifetime.Services
{
    public interface ITransientService : IBaseService
    {
    }
    
    public interface IScopedService : IBaseService
    {
    }
    
    public interface ISingletonService : IBaseService
    {
    }
}

All of them inherit from the IBaseService, which has only one string property Id:

C#
namespace DIServiceLifetime.Services
{
    public interface IBaseService
    {
        string Id { get; }
    }
}

Lets register a DemoService, which will inherit all interfaces. In this way we can register the DemoService for DI 3 times โ€“ with each of the inherited interfaces:

C#
namespace DIServiceLifetime.Services
{
    public class DemoService : ISingletonService, IScopedService, ITransientService
    {
        private readonly string id;

        public DemoService()
        {
            this.id = Guid.NewGuid().ToString();
        }

        public string Id => this.id;
    }
}

The DemoService has an Id, which comes from the IBaseService, and is setting this Id in its default constructor. That way we can keep track of how often the Guid Id changes, meaning a new service instance is created.

Next step is to register our DemoService for DI. As I already mention it can be registered 3 times for each interface, in the Program.cs (or in the Startup.cs) class:

C#
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSingleton<ISingletonService, DemoService>();
builder.Services.AddTransient<ITransientService, DemoService>();
builder.Services.AddScoped<IScopedService, DemoService>();

Now each time ISingletonService is requested โ€“ DemoService singleton instance will be created, if ITransientService โ€“ transient instance and scoped for the IScopedService. Letโ€™s request these services, by using DI constructor injection in our HomeController:

C#
private readonly ISingletonService singletonService;

private readonly ITransientService transientService;

private readonly IScopedService scopedService;

public HomeController(
    ISingletonService singletonService, 
    ITransientService transientService, 
    IScopedService scopedService)
{
    this.singletonService = singletonService;
    this.transientService = transientService;
    this.scopedService = scopedService;
}

Now we have the 3 instances available for use in the controller, and we can easily access each instance Id property. With the HomeController we can easily check how each service Id behaves between requests, but still, the HomeController is the one and only object depending on our services. So, simularly letโ€™s create another class that will also depend on the 3 types of services, for instance – ComplexService:

C#
namespace DIServiceLifetime.Services
{
    public class ComplexService : IComplexService
    {
        private readonly ISingletonService singletonService;

        private readonly ITransientService transientService;

        private readonly IScopedService scopedService;

        public ComplexService(
            ISingletonService singletonService,
            ITransientService transientService, 
            IScopedService scopedService)
        {
            this.singletonService = singletonService;
            this.transientService = transientService;
            this.scopedService = scopedService;
        }

        public IScopedService ScopedService => this.scopedService;

        public ITransientService TransientService => this.transientService;

        public ISingletonService SingletonService => this.singletonService;
    }
}

Letโ€™s register this service as Scoped. We are not going to monitor its instance, but the instances of its dependents, so its registration type is not so important:

C#
builder.Services.AddScoped<IComplexService, ComplexService>();

And finally letโ€™s inject it into the HomeController constructor:

C#
private readonly ISingletonService singletonService;

private readonly ITransientService transientService;

private readonly IScopedService scopedService;

private readonly IComplexService complexService;

public HomeController(
    ISingletonService singletonService, 
    ITransientService transientService, 
    IScopedService scopedService,
    IComplexService complexService)
{
    this.singletonService = singletonService;
    this.transientService = transientService;
    this.scopedService = scopedService;
    this.complexService = complexService;
}

In our HomeController we have 2 action methods Index and Data:

C#
public IActionResult Index(IDictionary<string, string> model)
{
    return View(model);
}

[HttpGet]
public IActionResult Data()
{
    var servicesData = new Dictionary<string, string>();
    servicesData.Add("SingletonService", this.singletonService.Id);
    servicesData.Add("TransientService", this.transientService.Id);
    servicesData.Add("ScopedService", this.scopedService.Id);
    ViewBag.ComplexService = this.complexService;
    
    return View("Index", servicesData);
}

The Data method is called from a form in the Index view:

C#
@{
    ViewData["Title"] = "Home Page";
}

<form method="GET" asp-controller="Home" asp-action="Data">
    <button type="submit" class="btn btn-success text-center">Get services IDs</button>
</form>

<partial name="/Views/Shared/_TablePartial.cshtml">

The Data method builds a dictionary with the service type as a Key and the service Id as a Value and passes it to the Index view, along with the ViewBag.ComplexService. And finally the index view renders the partial _TablePartial.cshtml view, which looks like this:

C#
@model Dictionary<string, string>;

@if (Model != null && @ViewBag.ComplexService != null)
{
    <table class="table">
        <thead>
            <tr>
                <th scope="col"></th>
                <th scope="col">Singleton</th>
                <th scope="col">Transient</th>
                <th scope="col">Scoped</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <th scope="row">From controller</th>
                @foreach (var item in Model)
                {
                    <td>@item.Value</td>
                }
            </tr>
            <tr>
                <th scope="row">From service</th>
                <td>@ViewBag.ComplexService.SingletonService.Id</td>
                <td>@ViewBag.ComplexService.TransientService.Id</td>
                <td>@ViewBag.ComplexService.ScopedService.Id</td>
            </tr>
        </tbody>
    </table>
}

The dictionary model, with the 3 service typesโ€™ Ids is, is rendered on the first table row, and the complex service Ids are rendered on the second row. When we run the application, this screen appears:

DI service lifetimes in .NET Demo
DI service lifetimes in .NET Demo

When clicking the button:

DI service lifetimes in .NET - example - first request
DI service lifetimes in .NET Results – 1st request

Now, in our application, we have two objects requesting our services โ€“ HomeController and ComplexService. Each of the 3 service types is injected into the two constructors. This is the DI constructor injection mechanism and .NET should provide an instance for each one of the injected services, in each one of the two objects.

Let’ summarize the results:

  • The Singleton service has the same Id in the controller and in the service โ€“ thatโ€™s because it is registered with AddSinglelton and .NET creates a single instance for this service, available during the whole application lifetime.
  • The Transient service is different for each object – .NET is creating a new instance each time such a service is requested.
  • The Scoped service is also the same for the two objects, because its instance is the same everywhere in the application, within the same request.

After that summary, let’s make another request, by clicking the button again:

DI service lifetimes in .NET - example - second request
DI service lifetimes in .NET Results – 2nd request
  • The Singleton is the same
  • The Transient changes every time
  • The Scoped also changes, compared to the first try, because we created a new request

In addition – be careful when nesting objects with different lifetime. If the outer object is with longer lifetime than the inner one, for instance – we have a singleton service requesting a transient in the constructor, the transient service wonโ€™t work as expected. The singleton constructor will be called only once so the transient service will be requested only the first time the singleton is created, and wonโ€™t have different instances among different requests. But if the singleton is the inner and the transient is the outer object โ€“ they will work just fine. So, just make sure your inner objects have equal or longer lifetime than the outer, where they are nested in.

Resources