Understanding the Dependency Injection Lifecycle: Singleton, Scoped, and Transient With Detailed Examples

Developers may be aware of the lifecycle of service instances when using dependency injection, but many don’t fully grasp how it works. You can find numerous articles online that clarify these concepts, but they often just reiterate definitions that you might already know. Let me illustrate with a detailed example that simplifies the explanation.

When implementing dependency injection, developers have three options that determine the life cycle of the instances:

  1. Singleton
  2. Scoped
  3. Transient

While most developers recognize these terms, a significant number struggle to determine which option to choose for a service’s lifetime.

Definitions

Let me start with definitions:

  • Singleton lifetime service instances are created once per application from the service container. A single instance will serve all subsequent requests. Singleton services are disposed of at the end of the application (i.e., upon application restart).
  • Transient lifetime service instances are created per request from the service container. Transient services are disposed of at the end of the request.
  • Scoped lifetime service instances are created once per client request. Transient services are disposed of at the end of the request.

When to Use

  • Singleton – When you want to use single instances of services throughout the life cycle of the application
  • Transient – When you want to use individual instances of services within the client request
  • Scoped – When you want to use a single instance of service for each request

What is a client request? In very simple words, you can consider it as an API/REST call coming to your application by button clicks of the user to get the response.

Don’t worry, let’s understand with an example.

Example

First, let’s create interfaces/services and classes:

C#

 

	// we are declaring 3 services as below
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

Now let’s write the implementation for each service Interface/service created above. We will try to understand the concept by trying to update the callMeSingleton, callMeTransient, and callMeScoped variable.

  • Singleton class implementation:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// other implementation
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// other implementation
}

  • Transient class implementation:
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// other implementation
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// other implementation
}

  • Scoped class implementation:
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	//other implementation
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	//other implementation		
}

Let’s register (ConfigureServices) with DI (Dependency Injection) to decide the life cycle of each service instance:

C#

 

services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();

Let’s use/call these services from 3 different classes (ClassA, ClassB, and ClassC) to understand the life cycle of each service:

  • ClassA:
C#

 

public class ClassA
{
	private ISingleton _singleton;
	//constructor to instantiate 3 different services we creates
	public ClassA(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassA()
	{
		_singleton.SetSingleton("I am from ClassA");
	} 
	
  	public void UpdateTransientFromClassA()
    {
		_transient.SetTransient("I am from ClassA");
	} 
	
  	public void UpdateScopedFromClassA()
	{
		_scoped.SetScoped("I am from ClassA");
	}

	// other implementation 
}

  • ClassB:
C#

 

public class ClassB
{
	private ISingleton _singleton;
	//constructor to instantiate 3 different services we creates
	public ClassB(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassB()
	{
		_singleton.SetSingleton("I am from ClassB");
	} 
	
  	public void UpdateTransientFromClassB()
    {
		_transient.SetTransient("I am from ClassB");
	} 
	
  	public void UpdateScopedFromClassB()
	{
		_scoped.SetScoped("I am from ClassB");
	}

	// other implementation 
}

  • ClassC:
C#

 

public class ClassC
{
	private ISingleton _singleton;
	//constructor to instantiate 3 different services we creates
	public ClassC(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassC()
	{
		_singleton.SetSingleton("I am from ClassC");
	} 
	
  	public void UpdateTransientFromClassC()
    {
		_transient.SetTransient("I am from ClassC");
	} 
	
  	public void UpdateScopedFromClassC()
	{
		_scoped.SetScoped("I am from ClassC");
	}

	// other implementation 
}

Analysis

Let’s analyze the results and behavior for each life cycle one by one from the above implementation:

Singleton

All the classes (ClassA, ClassB, and ClassC) will use the same single instance of the SingletonImplementation class throughout the lifecycle of the application. This means that properties, fields, and operations of the SingletonImplementation class will be shared among instances used on all calling classes. Any updates to properties or fields will override previous changes.

For example, in the code above, ClassA, ClassB, and ClassC are all utilizing the SingletonImplementation service as a singleton instance and calling SetSingleton to update the callMeSingleton variable. In this case, there will be a single value of the callMeSingleton variable for all requests trying to access this property. Whichever class accesses it last to update will override the value of callMeSingleton.

  • ClassA – It will have its same instance as other classes for service TransientImplementation
  • ClassB – It will have its same instance as other classes for service TransientImplementation.
  • ClassC – It will have its same instance as other classes for service TransientImplementation.

ClassA, ClassB, and ClassC are updating the same instance of the SingletonImplementation class, which will override the value of callMeSingleton. Therefore, be careful when setting or updating properties in the singleton service implementation.

Singleton services are disposed of at the end of the application (i.e., upon application restart).

Transient

All the classes (ClassA, ClassB, and ClassC) will use their individual instances of the TransientImplementation class. This means that if one class calls for properties, fields, or operations of the TransientImplementation class, it will only update or override its individual instance values. Any updates to properties or fields are not shared among other instances of TransientImplementation.

Let’s understand:

  • ClassA – It will have its own instance of service of TransientImplementation.
  • ClassB – It will have its own instance of service of TransientImplementation.
  • ClassC – It will have its own instance of service of TransientImplementation

Let’s say you have a ClassD which is calling transient service from ClassA, ClassB, and ClassC instances. In this case, each class instance would be treated as different/separate instance and each class would have its own value of callMeTransient. Read the inline comments below for  ClassD:

C#

 

public ClassD
{
	// other implementation
		
    // Below line of code will update the value of callMeTransient to "I am from ClassA" for the intance of ClassA only.
    // And it will not be changed by any next calls from Class B or B class
	ClassA.UpdateTransientFromClassA(); 		
       
    // Below line of code will update the value of callMeTransient to "I am from ClassB" for the intance of ClassB only.
    // And it will neither override the value for calssA instance nor will be changed by next call from Class C
	ClassB.UpdateTransientFromClassB(); 
    
    // Below line of code will update the value of callMeTransient to "I am from ClassC" for the intance of ClassC only.
    // And it will neither override the value for calssA and classB instance nor will be changed by any next call from any other class.
    ClassC.UpdateTransientFromClassC(); 

	// other implementation
}

Transient services are disposed at the end of each request. Use Transient when you want a state less behavior within the request.

Scoped

All the classes (ClassA, ClassB, and ClassC) will be using single instances of ScopedImplementation class for each request. This means that calls for properties/fields/operations on ScopedImplementation class will happen on single instance with in the scope of request. Any updates of properties/fields will be shared among other classes.   

Let’s understand:

  • ClassA – It will have its instance of service of TransientImplementation
  • ClassB – It will have its same instance of service of TransientImplementation as ClassA.
  • ClassC – It will have its same instance of service of TransientImplementation as ClassA and ClassB.

Let’s say you have a ClassD which is calling scoped service from ClassA, ClassB, and ClassC instances. In this case, each class will have single instance of ScopedImplementation class. Read the inline comments for ClassD below.

C#

 

public class ClassD
{
  // other implementation
 
  // Below code will update the value of callMeScoped to "I am from ClassA" for the instance of ClassA
  // But as it is Scoped life cycle so it is holding single instance ScopedImplementation of
  // Then it can be overridden by next call from ClassB or ClassC
	ClassA.UpdateScopedFromClassA();  
  
  // Below code will update the value of callMeScoped to "I am from ClassB" for single instance ScopedImplementation 
  // And it will override the value of callMeScoped for classA instance too. 
	ClassB.UpdateScopedFromClassB();
  
  // Now if Class A will perform any operation on ScopedImplementation,
  // it will use the latest properties/field values which are overridden by classB.
	
  // Below code will update the value of callMeScoped to "I am from ClassC"
  // And it will override the value of callMeScoped for classA and ClassB instance too.
	ClassC.UpdateScopedFromClassC(); 
  // now if Class B or Class A will perform any operation on ScopedImplementation , it will use the latest properties/field values which are overridden by classC

    // other implementation
}

Scoped services are disposed at the end of each request. Use Scoped when you want a stateless behavior between individual requests.

Trivia Time

The lifecycle of a service can be overridden by a parent service where it gets initialized. Confused? Let me explain:

Let’s take the same example from above classes and initialize the Transient and Scoped services from SingletonImplementation (which is a singleton) as below. That would initiate the ITransient and IScoped services and overwrite the lifecycle of these to singleton life cycle as parent service. In this case your application would not have any Transient or Scoped services (considering you just have these 3 services we were using in our examples). 

Read through the lines in the below code:

C#

 

public class SingletonImplementation: ISingleton
{
	// constructor to add initialize the services.
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // Now _transient would behave as singleton service irrespective of how it was registered as Transient
		_scoped = scoped;
        // now scoped  would behave as singleton service irrespective of it being registered as Scoped
	}
    var callMeSingleton = ""
		
	// other implementation
}

Summary

I hope the above article is helpful in understanding the topic. I would recommend try it yourself with the context set above and you will never be confused again. Singleton is the easiest to understand because once you create its instance, it will be shared across applications throughout the lifecycle of the application. On the similar lines of Singleton, Scoped instances mimic the same behavior but only throughout the lifecycle of a request across application. Transient is totally stateless, for each request and each class instance will hold its own instance of serivice.

Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle