What are Service Lifetimes in ASP.NET Core DI

In this article, let's talk in depth about the different services in ASP.NET Core DI based on the lifetimes and everything about them.

Dependency Injection is one of the most important and interesting features of .NET Core, which simplifies the way dependencies for any component are instantiated and maintained for use.

In the DI approach, a class is not instantiated explicitly using the new keyword. Instead, the Startup class is configured about how a type shall be instantiated when requested by a component. The types registered in this way are called “services” and are injected wherever necessary through the constructor. These services implement the IoC (Inversion of Control) approach by design.

The DI container takes care of instantiation and memory management of these services, and disposes when their configured lifetime is over. All the dependencies and references created within a service are also released accordingly when a service is disposed.

When we would want any component to use an instance of the type which is registered as a service, we add a constructor parameter of the registered type inside the component and the DI container passes an instance of the type during runtime.

Example

public interface IFactorialService
{
	double Calculate(int n);
}

public class FactorialService : IFactorialService
{
	public double Calculate(int n) 
	{
		//logic to calculate factorial of n.
	}
}

public class FactorialClient 
{
	private IFactorialService _service;

	// The service instance is injected 
	// into the component when it is constructed and
	// the constructor is invoked during runtime
	
	public FactorialClient(IFactorialService service) 
	{
		_service = service;
	}

	// functionality where the 
	// injected service can be used
	public void ShowFactorial(int n) 
	{
		Console.WriteLine(_service.Calculate(n));
	}
}

When a type is registered as a service for injection, we would also need to specify for how long an instance of that service once created can be retained for reuse and how it can be reused. This specification is called as defining the “Service Lifetime”.

There are three types of services differentiated by their “Service Lifetime”. They are:

  1. Singleton
  2. Transient
  3. Scoped

Singleton Services

If a service is defined as a Singleton service, the container creates a new instance only “once” and all the consequtive requests for the instances of that type are resolved with that “one” instance, till the end of the application. One can think of a Singleton service as a kind of “static” types which are instantiated only once and are reused for all times.

A Singleton service is registered in .NET Core during Startup as:

public void ConfigureServices(IServiceCollection services) 
{
	services.AddSingleton<IFactorialService, FactorialService>();
}

When registering services are “Singletons” one must keep the following things in mind:

Since Singletons are instantiated “only once” in the entire life cycle, they can contribute to overall application performance.
Since Singletons stay forever in the application untill it is terminated, any object created inside the service if not disposed properly can create potential Memory leaks. Hence it is responsiblity of the developers to ensure the objects created inside Singletons are disposed properly.
A type should be registered as a “Singleton” only when it is fully thread-safe and is not dependent on other services or types.

Scoped Services

If a service is defined as Scoped, it is instantiated only once per “scope”. A scope can be assumed to be a bound within which a Scoped service instance can be created “once” and reused as many times required. But once when we are out of this “bound”, a new instance of the service is created and served to us. In ASP.NET Core, every web request is bound to one single “scope” and hence when a service is created as a “Scoped Service”, it is instantiated “once within the request” and reused within “that request scope” only.

For example, let’s say we have a service TimestampService that returns the current timestamp whenever invoked. Let’s assume we have registered this service with its abstraction ITimestampService as a “Scoped Service”.

When we invoke this service in various places inside our application within “one request”, we should get the same Timestamp that is recorded the first time the service instance is created.

namespace ScopedServices 
{
	public interface ITimestampService 
	{
		long GetCurrentTimestamp();
	}

	public class TimestampService : ITimestampService 
	{
		public long GetCurrentTimestamp() 
		{
			return DateTimeOffset.Now.ToUnixTimeSeconds();
		}
	}
}
public class DemoController
{
	private readonly ITimestampService service;
	
	public DemoController(ITimestampService service) 
	{
		this.service = service;
	}

	public IActionResult Index() 
	{
		return View(this.service.GetCurrentTimestamp());
	}
}
@model long
@inject ScopedServices.ITimestampService service

<p>Injected from constructor: @Model</p>
<p>Injected inside the View @service.GetCurrentTimestamp()</p>

When this code is executed, one can observe that the values printed in both the tags are exactly the same, since it is the same service they are accessing.

One common usage of this approach would be any database logic implementing classes, since one connection can be allowed within a request. The DbContext class of the Entity Framework core, when declared would result in a scoped instance.

Example

public void ConfigureServices(IServiceCollection services) 
{
	services.AddScoped<ITimestampService, TimestampService>();
}

Understanding Scope with an example

By definition, Scoped services instances are “created once and reused inside a scope”. This means that if we resolve an instance of a Scoped service inside another “scope” other than the “request scope”, we get a new instance resolved.

To create a scope, we make use of the instance of IServiceProvider, which is responsible for maintaining the services. We can then create a ServiceScope using the instance of the IServiceProvider and resolve an instance of the service ITimestampService and see if it brings in any difference.

public class DemoController : Controller
{
	private readonly ITimestampService service;
    private readonly IServiceProvider serviceProvider;

    public DemoController(
        ITimestampService service, 
        IServiceProvider serviceProvider)
    {
        this.service = service;
        this.serviceProvider = serviceProvider;
    }

	public IActionResult Index()
    {
        using (var scope = serviceProvider.CreateScope())
        {
			// container resolves an instance for 
			// the requested service type
        	var tsService = scope.ServiceProvider
				.GetRequiredService<ITimestampService>();
            
			// pass this value in the ViewBag
			// the values passed in Model and Injected in View
			// should return the same value which is different 
			// from the value returned in the ViewBag
			ViewBag.Timestamp = tsService.GetCurrentTimestamp();
        }

        return View(this.service.GetCurrentTimestamp());
    }
}
@model long
@inject ScopedServices.ITimestampService service

<p>Injected from constructor: @Model</p>
<p>Injected inside the View @service.GetCurrentTimestamp()</p>
<p>Injected from ServiceScope: @ViewBag.Timestamp</p>

When you run this code, one can observe that the values returned are different as expected. This explains that scoped services are bound under a scope, and a new instance is created and reused inside a created “scope”.

Transient Services

Transient services almost mimic the functioning of a typical class instantiation and disposal. If a service is defined as Transient, it is instantiated whenever invoked within a request. It is almost similar to creating an instance of the same type using “new” keyword and using it. It is also the safest option among all other service types, since we don’t need to bother about the thread-safety and memory leaks.

In the previous example, if ITimestampService was defined as a Transient service, a new instance of ITimestampService is resolved each time it is requested in the application, which leads to three different values of Timestamps printed on the View.

When one is confused about which service type to be chosen for a class, begin by making it a Transient.

One common usage of this approach would be any lightweight logical classes which can hold temporary data. One common example can be a controller, which is internally a Transient service instantiated per request.

Example:

services.AddTransient<IMyService, MyService>();

Should only interfaces be used for Registering services?

Not necessarily. DI is all about configuring what would be requested by the component and what to inject in its place. It has nothing much to do about how the types are mapped.

You may register any given type to be injected in place of an Interface as its concrete implementation, for an abstract base class as its extension, or the type itself and use the same type to inject in the constructor.

#Resolving an Abstract Type into its Derived Type via Injection#

public abstract class TimestampServiceBase 
{
    public abstract long GetCurrentTimestamp();
}

public class TimestampService : TimestampServiceBase
{
	public override long GetCurrentTimestamp()
    {
            return DateTimeOffset.Now.ToUnixTimeSeconds();
    }
}

public class DemoController : Controller
{
	private readonly TimestampServiceBase service;
    
    public DemoController(TimestampServiceBase service)
    {
    	this.service = service;
    }
}

The DI constructor looks for what is specified in the constructor params, and checks its registry for “what needs to be substituted for its type” and then substitutes an instance of the mapped type.

services.AddSingleton<TimestampServiceBase, TimestampService>();

There’s only one rule here: “the types mentioned must be related to one another – a base and derived class or an interface and its implementation.”

Injecting Services into one Another

There can be times when one service depends on the other for their responsiblity – be it a DbContext used or a Logging functionality be invoked. When a service requests instance of another within its constructor, the DI container resolves the requested according to its lifetime and available instances. This can also mean that the lifetime of the service being injected can impact on the lifetime of the dependent service.

A long-lived service (say like singleton) cannot invoke a short-lived service (say like transient) within its constructor. If a long-life service is injected into a short-life service, it can cause issues with the functionality of the long-life service since it might end up referencing a service which was already disposed for its lifetime and this breaks the application. The .NET Core DI container handles this by throwing exceptions when the service tree is being constructed during the application boot up.

A Common example for such scenario is injecting DbContext within a singleton service, which results in an exception thrown during runtime. Hence all services injecting DbContext (which itself is a scoped service) can only be transient or scoped.

Example

public class MyService : IMyService {

	private ISomeOtherService _service;

	public MyService(ISomeOtherService service) 
	{
		_service = service;
	}

	public void ShowMessage(string message) 
	{
		_service.SomeLoggerImpl(message);
		Console.WriteLine(message);
	}

}	

In terms of lifetime, the singleton object gets the highest life per instantiation, followed by a Scoped service object and the least by a Transient object.

One way to solve this issue is by making use of Service Scopes inside the services which results in a clean injection and release of services without depending on the lifetime of the service in which the services are being resolved.

For example, to use DbContext inside a singleton LoggingService, we can create a ServiceScope and access the context.

public class LoggerService : ILoggerService
{
	private readonly IServiceProvider sp;

	public LoggerService(IServiceProvider sp)
	{
		this.sp = sp;
	}

	public void WriteLog(string message) 
	{
		using(var scope = this.sp.CreateScope())
		{
			var ctx = scope.ServiceProvider.GetRequiredService<MyDbContext>();
			ctx.Logs.Add(new Log {
				Message = message
			});
			ctx.SaveChanges();
		}
	}
}

The best way to resolve a service instance from the container and release it when it is no longer required in a service inside a method is by using the ServiceScope(). All the services resolved within a Scope() are released at the end of the Using() block.

This way, we can create, inject and resolve services inside an ASP.NET Core application using the native DI container along with ways to use one service inside another (such as a Scoped inside a Singleton) by creating scopes to resolve services, without having to worry about their lifetimes.

Complete Example is available at https://github.com/referbruv/servicelifetime-sample


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *