In this article, we’ll learn about Service Lifecycles in C# by examples. In C#, Dependency Injection (DI) containers or Inversion of Control (IoC) containers are responsible for creating and managing the lifecycle of objects, including how they are created, how they are disposed, and how they are shared between components.
Imagine that we have the following service:
class MyService { }
And the following component needs it.
class MyComponent { private readonly MyService _service; public MyComponent(MyService service) { _service = service; } }
And we want to let an Inversion of Control Container instantiate the service and the component.
In this example let’s use the dotnet’s IoC library. So install the Microsoft.Extensions.DependencyInjection
Nuget package.
And create a new instance of the service collection.
using Microsoft.Extensions.DependencyInjection; // Create a new service collection var services = new ServiceCollection();
There are three method of adding services to the collection. To see an example of each, let’s register MyService
as a singleton, and MyComponent
as a scoped for now.
// Add a singleton service services.AddSingleton<MyService>(); // Add a scoped service services.AddScoped<MyComponent>();
For now, we use the AddSingleton
and AddScoped
extension methods to register the services with the DI container. These methods determine the lifecycle of the service, so to speak.
- The
AddSingleton
method creates a single instance of the service for the entire application. AddScoped
creates a single instance of the service per scope (I’ll explain it later in this article).AddTransient
creates a new service instance each time it is requested (I’ll illustrate this later in this article).
You’ll see the difference better once we instantiate the services. But we need a service provider to instantiate the services. So let’s create one that can provide the dependency instances based on the configuration above.
// Create a new service provider var provider = services.BuildServiceProvider();
Singleton
We registered MyService
as a singleton. Now, if I use the provider
to create two instances of MyService
:
// Resolve a singleton service var service1 = provider.GetService<MyService>(); var service2 = provider.GetService<MyService>();
The instances are identical. I can prove it. 😉
// Both service1 and service2 should be the same instance Console.WriteLine($"service1 == service2: {service1 == service2}");
This is the console’s output:
service1 == service2: True
Scoped
If I create two instances of a scoped dependency:
using (var scope1 = provider.CreateScope()) { var component1 = scope1.ServiceProvider.GetService<MyComponent>(); var component2 = scope1.ServiceProvider.GetService<MyComponent>(); Console.WriteLine($"component1 == component2: {component1 == component2}"); }
Both variables refer to the same object within the same scope. And that’s how you can create scopes.
So the output is:
component1 == component2: True
To clarify what that means, let’s create each component instance in a different scope.
MyComponent component1; MyComponent component2; using (var scope1 = provider.CreateScope()) { component1 = scope1.ServiceProvider.GetService<MyComponent>(); } using (var scope2 = provider.CreateScope()) { component2 = scope2.ServiceProvider.GetService<MyComponent>(); }
And test it:
Console.WriteLine($"component1 == component2: {component1 == component2}");
So the output is:
component1 == component2: False
So, scopes help us separate environments in which the instances are shared. You can resolve the service using the GetService
method and control the scope of the service by creating a scope using the CreateScope
method and resolving the service in that scope.
Transient
Transient is the simplest one. Each time it creates a new instance regardless of the scope. This is how I can register a transient dependency:
services.AddTransient<MyComponent>();
Now, if I ask the provider for the service twice:
// Resolve a transient service var component3 = provider.GetService<MyComponent>(); var component4 = provider.GetService<MyComponent>();
Guess the output of the following test 😃
Console.WriteLine($"component3 == component4: {component3 == component4}");
Right! `component3` and component4
should refer to different instances. So the output is:
component3 == component4: False
By the way, did you know that we offer a unique online course that boosts your C# career? Check it out here!
Conclusion
There are three methods for service dependency resolution when using Dotnet’s built-in IoC library.
- The
AddSingleton
method creates a single instance of the service for the entire application. AddScoped
creates a single instance of the service per scope.AddTransient
creates a new service instance each time it is requested.
ASP.NET creates a scope per request. If you register a dependency as scoped, ASP.NET shares the instances of that dependency with all clients (clients are the objects that need the dependency in the Object-Oriented programming context).