Experimenting With Service Lifetimes In .NET Core

In this article we'll have a look at different lifetime options we have registering service via built-in IoC container provided in .net core. As an example we'll use code provided in one of my previous articles.
 
To quiclky recap we have a Quartz.Net job which depends on a service.
  1. private readonly IDemoService _demoService;    
  2. public DemoJob(IDemoService demoService)    
  3. {    
  4.     _demoService = demoService;    
  5. }   
Instead of injecting DemoService directly we provide IDemoService abstraction which DemoJob depends upon.
 

Understanding service lifetimes

 
In the abovementioned article, we have registered our services with scoped lifetime.
  1. var serviceCollection = new ServiceCollection();  
  2. serviceCollection.AddScoped<DemoJob>();  
  3. serviceCollection.AddScoped<IDemoService, DemoService>();  
  4. var serviceProvider = serviceCollection.BuildServiceProvider();  
However, there is no actual thinking presented here as to why we have chosen it over other options such as transient or singleton lifetime.
 
Let’s examine the other options. In order to achieve this, we’ll add some trace statements to our class constructors.
  1. public DemoService()  
  2. {  
  3.     Console.WriteLine("DemoService started");  
  4. }  
And the job constructor:
  1. public DemoJob(IDemoService demoService, IOptions<DemoJobOptions> options)  
  2. {  
  3.     _demoService = demoService;  
  4.     _options = options.Value;  
  5.     Console.WriteLine("Job started");  
  6. }  
The service registration is as follows,
  1. serviceCollection.AddTransient<DemoJob>();  
  2. serviceCollection.AddTransient<IDemoService, DemoService>();  
After we run the program we’ll observe the following output,
 
DemoService started
Job started
calling http://i.ua
DemoService started
Job started
calling http://i.ua
DemoService started
Job started
calling http://i.ua
 
The output is pretty self-explanatory: We create a new instance each time we call service. Changing both registrations to AddScoped or AddSingleton produces the same result,
 
DemoService started
Job started
calling http://i.ua
calling http://i.ua
calling http://i.ua
 
Both instances are constructed just once at application startup. Let’s consult with the documentation to see what are the difference between those lifetimes and why the produce the same result for a given example.
 
Scoped lifetime services are created once per client request (connection).
 
Here is what singleton does.
 
Singleton lifetime services are created the first time they’re requested.
 
So in our case, we have a single request because we use console application. This is the reason why both service lifetimes act the same.
 
The last topic most of DI-related articles do not cover is a composition of services with different lifetimes. Although there is something worth mentioning. Here is the example of registration.
  1. serviceCollection.AddSingleton<DemoJob>();  
  2. serviceCollection.AddTransient<IDemoService, DemoService>();  
This  means that we inject transient dependency into singleton service. One might expect that since we declared IDemoService as transient it will be constructed each time.
 
The output, however, is quite different,
 
DemoService started
Job started
calling http://i.ua
calling http://i.ua
calling http://i.ua
 
So again both services are constructed at the application startup. Here we see that lifetime of transient service gets promoted by the service that uses it. This leads to an important application. The service we’ve registered as transient might be not be designed to be used as a singleton because it is not written in thread-safe fashion or for some other reasons. However, it becomes singleton in this case which may lead to some subtle bugs. This brings us to the conclusion that we shouldn’t register services as singletons unless we have some good reason for it; i.e., service that manages global state. It’s preferable to register services as transient.
 
The opposite, however, yields no surprises.
  1. serviceCollection.AddTransient<DemoJob>();  
  2. serviceCollection.AddSingleton<IDemoService, DemoService>();  
produces
 
DemoService started
Job started
calling http://i.ua
Job started
calling http://i.ua
Job started
calling http://i.ua
 
Here each new instance of a job reuses the same singleton DemoService.