Life cycle of Dependency Injection

Understanding the lifecycle of services created using Dependency Injection is very important before using them. Creating services without understanding the difference between Transient, Singleton, and Scoped can cause the system to not work as expected. When a service requests another service through DI, it is important to know whether it received a new instance or an existing instance. Therefore, it is very important to be aware of the correct life cycle or life time while registering a service.

Service lifecycle management

Whenever we request a service, the DI Container decides whether to create a new instance or reuse the previously created instance. The service life cycle depends on when the instance is instantiated and how long it lives. We define the lifecycle when we register the service.

We learned how to use DI in the previous lesson. There are 3 lifecycle levels, this way we can decide how each service has a lifecycle.

Transient: A new instance is always created, every time it is requested.

Scoped: Create a new instance for all scopes (Each request is a scope). In scope, the service is reused

Singleton: Service is created only once.

Project example

Create a new Service Interface Create a new service named SomeService in the Services folder. Add 3 new interfaces. Each interface corresponds to a lifecycle in ASP.NET Core. The interface is simple, they contain each GetID method that returns a Guid.

public interface ITransientService 
{ 
    Guid GetID();
}
 
public interface IScopedService 
{ 
   Guid GetID();
}

public interface ISingletonService 
{ 
   Guid GetID();
}

Create a Service Now we will create a single service that implements all three interfaces:

public class SomeService : ITransientService, IScopedService, ISingletonService 
{ 
    Guid id;
     
    public SomeService() 
    { 
        id = Guid.NewGuid(); 
    }
    public Guid GetID()
    {
        return id;
    }
}

The service generates a unique ID and it will be initialized and return the id in the GetID method. Now let's see the details

Transient

The Transient Service always creates a new one each time the service is requested

Sign up for Transient Service

Now inside the ConfigureServices method of Startup will register SomeService through the ITransientService interface.

services.AddTransient<ITransientService, SomeService>();

Inject into Controller

Open the HomeController and inject 2 instances of SomeService as shown below:

public class HomeController : Controller
{
    ITransientService _transientService1;
    ITransientService _transientService2;

    public HomeController(ITransientService transientService1,
                          ITransientService transientService2)
    {
        _transientService1 = transientService1;
        _transientService2 = transientService2;
    }

    public IActionResult Index()
    {

        ViewBag.message1 ="First Instance " + _transientService1.GetID().ToString();
        ViewBag.message2 ="Second Instance "+ _transientService2.GetID().ToString();

        return View();
    }
}

First, we inject two service instances through the ITransientService interface in the HomeController constructor.

public HomeController(ITransientService transientService1,ITransientService transientService2)

Next, we call the GetID method in each instance and assign it to the view using ViewBag.

ViewBag.message1 ="First Instance " + _transientService1.GetID().ToString();
ViewBag.message2 ="Second Instance "+ _transientService2.GetID().ToString();

View

<h3>Transient Service</h3>
@ViewBag.message1
</br>
@ViewBag.message2

Run the application and you will see 2 different Guids displayed on the screen. The proof is that we got 2 instances of the Transient service.

Scoped

Services with scoped lifecycle are created only once per request (scope). A new instance is created for each request and the instance is reused in the request.

Sign up for Scoped Service

In the ConfigureServices method register SomeService using the AddScoped method using the IScopedService interface.

services.AddScoped<IScopedService, SomeService>();

Inject scoped service into Controller

Next inject this service into the Controller. We already have a transient service injected into the controller. Now nothing has changed but added a new one:

public class HomeController : Controller
{
    ITransientService _transientService1;
    ITransientService _transientService2;

    IScopedService _scopedService1;
    IScopedService _scopedService2;

    public HomeController(ITransientService transientService1,
                          ITransientService transientService2, 
                          IScopedService scopedService1,
                          IScopedService scopedService2)
    {
        _transientService1 = transientService1;
        _transientService2 = transientService2;

        _scopedService1 = scopedService1;
        _scopedService2 = scopedService2;
    }

    public IActionResult Index()
    {
        ViewBag.message1 ="First Instance " + _transientService1.GetID().ToString();
        ViewBag.message2 ="Second Instance "+ _transientService2.GetID().ToString();

        ViewBag.message3 = "First Instance " + _scopedService1.GetID().ToString();
        ViewBag.message4 = "Second Instance " + _scopedService2.GetID().ToString();

        return View();
     }
}

Add to action method 2 variables message3 and message4 for scoped service.

View

In view add 3 lines

<h3>Scoped Service</h3>
@ViewBag.message3
</br>
@ViewBag.message4

Run the application. The instance is created only once per request, that's why we create 2 identical IDs. Now refresh the browser. The ID has changed because a new instance is created for each request.

Singleton

A single instance of the service is created when it is first requested. Then each subsequent request will only use that same instance. The new request does not create a new instance, it is reused.

Register Singleton Service

Singleton service is registered using the AddSingleton

services.AddSingleton<ISomeServiceService, SomeService>();

Inject Singleton service into Controller

public class HomeController : Controller
{
    ITransientService _transientService1;
    ITransientService _transientService2;

    IScopedService _scopedService1;
    IScopedService _scopedService2;

    ISingletonService _singletonService1;
    ISingletonService _singletonService2;

    public HomeController(ITransientService transientService1,
                      ITransientService transientService2, 
                      IScopedService scopedService1,
                      IScopedService scopedService2,
                      ISingletonService singletonService1,
                      ISingletonService singletonService2)
    {

        _transientService1 = transientService1;
        _transientService2 = transientService2;

        _scopedService1 = scopedService1;
        _scopedService2 = scopedService2;

        _singletonService1 = singletonService1;
        _singletonService2 = singletonService2;
     }

     public IActionResult Index()
     {
         ViewBag.message1 ="First Instance " + _transientService1.GetID().ToString();
         ViewBag.message2 ="Second Instance "+ _transientService2.GetID().ToString();

         ViewBag.message3 = "First Instance " + _scopedService1.GetID().ToString();
         ViewBag.message4 = "Second Instance " + _scopedService2.GetID().ToString();

         ViewBag.message5 = "First Instance " + _singletonService1.GetID().ToString();
         ViewBag.message6 = "Second Instance " + _singletonService2.GetID().ToString();

         return View();
     }
}

First we inject 6 instances of SomeService. Two instances for each interface.

View

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<h3>Transient Service</h3>
@ViewBag.message1
</br>
@ViewBag.message2

<h3>Scoped Service</h3>
@ViewBag.message3
</br>
@ViewBag.message4

<h3>Singleton Service</h3>
@ViewBag.message5
</br>
@ViewBag.message6

Run the application and you will see that the IDs generated from the Singleton service are always the same and will not change even if you refresh the application. You can see in the picture below

So which one should be used?

Transient service is the safest way to create, since you are always creating a new instance. But because of that it will create each time you request so will use more memory and resources. This may adversely affect performance if too many instances are created.

Using Transient Service is suitable when you want to use for lightweight and small and stateless services.

Scoped service is better when you want to maintain state within a request.

Singleton is created only once, it is not destroyed until application shutdown. Any memory usage with these services builds up over time and it fills up. But also helps us save memory if handled well because they are created only once and used everywhere.

Use Singleton when you need to maintain system-wide state. Configuration, application parameters, service logging, data caching... are common examples of Singleton usage.

Inject service with different lifecycle into another service

Be careful, when injecting services into other services with different lifecycles. Let's see an example Singleton Service that depends on another service registered with a lifecycle of Transient.

When the request first arrives, an instance of the Singleton is created.

When the second request arrives, this instance of the Singleton is reused. And this singleton already contains an instance of the transient service. So it is not recreated. This invisibly converted a transient service into a singleton service.

Services with a lower lifecycle injected into a service with a higher lifecycle will change a lower lifecycle service to a higher lifecycle service. This will make debugging harder and it should be avoided. From low to high are Transient, Scoped and Singleton.

So remember the rule:

  • Never inject Scoped & Transient service into Singleton service

  • Never inject Transient Service into Scope Service

Last updated