Dependency Injection Service Lifetimes In .NET 6.0

In this tutorial, we are going to discuss the different lifetime's services in .NET 6.0.

In Dependency Injection container, an object can be registered either of the three different ways,

  1. Transient
    An object is created whenever they are requested from the container. It does not matter how many times the object is being called. Every time it is called, a new instance will be created, and it will be returned from the container. The transient objects are disposed at the end of request.
     
  2. Scoped
    The scope lifetime basically indicates that the instance of the object that we are requesting from the container will be created once per the request lifecycle. Meaning, once per an HTTP request. Even if we call multiple times, the same instance will be returned across the lifetime of the request. The object will be disposed at the end of the request. We will investigate in detail regarding this in our example.
     
  3. Singleton
    Object will be created only once for the entire lifetime of the application. Every subsequent request, container will return the same instance. We will see the details in a demonstration.

Note
One thing we need to keep in mind regarding how these different objects should be created or order of the operations. In terms of order of operations.

  • A singleton service can be resolved from a singleton or scoped or transient service.
  • A scoped service can be resolved from a scoped or transient service.

One thing we should be careful about is not to resolve a scoped service from a singleton service. Because it might have unwanted consequences on how we use the scoped service.

Without any delay, let us go ahead and create a ASP.NET Core Web API project.

The tools which I’m using for this demo are below,

  1. Visual Studio Community 2022(64 bit) – Preview - Version 17.4.0 Preview 2.1
  2. .Net 6.0
  3. Web API
  4. Swagger
  5. Postman

The source code can be downloaded from the GitHub

Let us create the below interfaces.

ICounter.cs

namespace LifetimeDemo.Interfaces {
    public interface ICounter {
        void Increment();
        int GetCount();
    }
}

IFirstInstance.cs

namespace LifetimeDemo.Interfaces {
    public interface IFirstInstanace {
        int IncrementAndGet();
    }
}

ISecondInstance.cs

namespace LifetimeDemo.Interfaces {
    public interface ISecondInstance {
        int IncrementAndGet();
    }
}

Now let us go ahead and create the implementation classes

Counter.cs

using LifetimeDemo.Interfaces;
namespace LifetimeDemo.Implementations {
    public class Counter: ICounter {
        private int _count;
        public int GetCount() => _count;
        public void Increment() => _count++;
    }
}

FirstInstance.cs

using LifetimeDemo.Interfaces;
namespace LifetimeDemo.Implementations {
    public class FirstInstance: IFirstInstanace {
        private ICounter _counter;
        public FirstInstance(ICounter counter) => _counter = counter;
        public int IncrementAndGet() {
            _counter.Increment();
            return _counter.GetCount();
        }
    }
}

SecondInstance.cs

using LifetimeDemo.Interfaces;
namespace LifetimeDemo.Implementations {
    public class SecondInstance: ISecondInstance {
        private ICounter _counter;
        public SecondInstance(ICounter counter) => _counter = counter;
        public int IncrementAndGet() {
            _counter.Increment();
            return _counter.GetCount();
        }
    }
}

Now create a Controller CounterController.cs

using LifetimeDemo.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace LifetimeDemo.Controllers {
    [Route("api/[Controller]")]
    [ApiController]
    public class CounterController: ControllerBase {
        private readonly IFirstInstanace _first;
        private readonly ISecondInstance _second;
        public CounterController(ISecondInstance second, IFirstInstanace first) => (_second, _first) = (second, first);
        [HttpGet]
        public int Get() {
            _first.IncrementAndGet();
            return _second.IncrementAndGet();
        }
    }
}

In the CounterController.cs, if you look at the Method “Get”, I’m incrementing the count using the _first instance and, returning the value using the _second instance.

The final step is to register the objects into container through dependency injection. As we are using .NET 6.0, Program.cs is the file, where we are going to register the interfaces.

//Add Transient Service
builder.Services.AddTransient < ICounter, Counter > ();
builder.Services.AddTransient < IFirstInstanace, FirstInstance > ();
builder.Services.AddTransient < ISecondInstance, SecondInstance > ();
//Add Scoped Service
//builder.Services.AddScoped<ICounter, Counter>();
//builder.Services.AddTranient<IFirstInstanace, FirstInstance>();
//builder.Services.AddTranient<ISecondInstance, SecondInstance>();
//Add Singleton Service
//builder.Services.AddSingleton<ICounter, Counter>();
//builder.Services.AddTransient<IFirstInstanace, FirstInstance>();
//builder.Services.AddTransient<ISecondInstance, SecondInstance>();

We will start with Transient.

Now let us go ahead and run the application and see the response. I will be using Swagger for demo purpose.

Run the application and click on Execute on Counter API.

The response would be “1”

Though we are using the ICounter as a state management, we are registering the service as Transient, each time, the ICounter instance would be created a new one and return from the container.

To understand how this works, put break points in GetIncrementAndGet() method in FirstInstance.cs and SecondInstance.cs classes as mentioned below and debug.

Dependency Injection Service Lifetimes in .NET 6.0

Now, we will look at Scoped Service. Let us go ahead and uncomment the Scoped Service in Program.cs as below

//Add Transient Service
//builder.Services.AddTransient<ICounter, Counter>();
//builder.Services.AddTransient<IFirstInstanace, FirstInstance>();
//builder.Services.AddTransient<ISecondInstance, SecondInstance>();
//Add Scoped Service
builder.Services.AddScoped < ICounter, Counter > ();
builder.Services.AddTransient < IFirstInstanace, FirstInstance > ();
builder.Services.AddTransient < ISecondInstance, SecondInstance > ();
//Add Singleton Service
//builder.Services.AddSingleton<ICounter, Counter>();
//builder.Services.AddTransient<IFirstInstanace, FirstInstance>();
//builder.Services.AddTransient<ISecondInstance, SecondInstance>();

Run the application and see the result.

Dependency Injection Service Lifetimes in .NET 6.0

Here the result is “2”. Since the ICounter has been registered as “Scoped”, the same instance will be returned from container. Execute the endpoint again, we will get the same response “2”. Each time, a new instance will be created at the HTTP request level.

Put break points in GetIncrementAndGet() method in FirstInstance.cs and SecondInstance.cs classes as mentioned below and debug.

When to use Transient Vs Scoped?

  • Transient can be used when you do not want to maintain any state inside of a class.
  • Scoped can be used when you want to maintain the state across different calls inside the lifetime of a particular http request

Finally, let us try the Singleton. Make the necessary changes in Program.cs as below

//Add Transient Service
//builder.Services.AddTransient<ICounter, Counter>();
//builder.Services.AddTransient<IFirstInstanace, FirstInstance>();
//builder.Services.AddTransient<ISecondInstance, SecondInstance>();
//Add Scoped Service
//builder.Services.AddScoped<ICounter, Counter>();
//builder.Services.AddTransient<IFirstInstanace, FirstInstance>();
//builder.Services.AddTransient<ISecondInstance, SecondInstance>();
//Add Singleton Service
builder.Services.AddSingleton < ICounter, Counter > ();
builder.Services.AddTransient < IFirstInstanace, FirstInstance > ();
builder.Services.AddTransient < ISecondInstance, SecondInstance > ();

Now execute the application and see the response.

Dependency Injection Service Lifetimes in .NET 6.0

The first response would be “2”, execute again and see the response

Dependency Injection Service Lifetimes in .NET 6.0

The response becomes "4". If you execute again, you will get "6". Irrespective of how many times we are executing the API, you will be the same instance from container. As we have registered ICounter as singleton, the object will be created once per the application lifetime and will return same from the container each time when we are requesting them.

You try the same API (api/counter) from different browsers (Edge, Chrome, etc) or even from Postman, we will get the response incremented by 2- this will prove that the same instance would be returning for multiple HTTP requests.

When to use Singleton?

If we want to keep the state of an object across requests for the lifetime of an application, singleton would be right candidate.

As of now, we have seen the different lifetimes of objects. I hope, you have got an understanding on what lifetime needs to be chosen based on our requirements. Thank you for reading my article and leave your comments in the comments box below.