Dependency Injection and Service Lifetimes in .NET Core
Dependency Injection (DI) is a design pattern which supports the development of loosely coupled code, and it’s also one of the SOLID principles (Dependency Inversion Principle). On this article, I will explain how we can use Dependency Injection and what is the difference between the three services lifetimes that can be used in .NET Core applications.
“Dependency Injection (DI) is a technique that provides dependencies to a class, thereby achieving dependency inversion. Dependencies are passed (injected) to a client that needs it.”
DI is used a lot in .NET Core applications, and also the own framework brings it natively. Using Dependency Injection brings a lot of benefits like:
- Removes hardcoded dependencies and allows them to be changed
- Allows the developer to write loosely coupled code, making applications easier to maintain, extend, and test
- Increase the testability in software applications that use DI. Loosely coupled code can be tested more easily. It’s easier to make a change in the code without affecting other areas of the application. With DI it’s possible to inject mocked dependencies in the tests.
- Developers can work on different pieces of functionality in parallel since the implementations are independent of each other.
- Improve the readability and cleanliness of the code
Example of Dependency Injection
On the code below, there is the ICategoryRepository interface and the class CategoryRepository which implements this interface:
The interface is like a contract. So the class who implements this interface should implement all methods that this interface demands. For example, if the CategoryRepository class does not implement the GetCategory method, it will show an error, because the interface demands that this method need to be implemented in the class.
The services should be registered into the ConfigureServices method in the Startup class, which on the example below we are setting that there is the interface ICategoryRepository and through this interface, it will be resolved and will get an instance of CategoryRepository class:
With this configuration, always when we need an instance of this class, it will be created for us. Now, it’s possible to use the CategoryRepository class in the CategoryService class, using DI through the constructor of the class:
This constructor accepts an ICategoryRepository parameter and it stores the dependency into a private ready only field. Setting this property as a read-only property will avoid the possibility of other methods accidentally assign a different value for this dependency. This class can now make use of this abstraction.
With DI, the code is not depending on a concrete class (the code does not depends on the class “CategoryRepository”) but depends on an abstraction (the “ICategoryRepository). The CategoryService is not using the concrete type “CategoryRepository”, only use the “ICategoryRepository” interface it implements.
This makes it easy to change the implementation of the CategoryRepository, which is used by the service class, without modifying the service class itself. Also, the service class does not create an instance of “CategoryRepository”, it is only created by the DI container. This way the code is loosely coupled, so even if something is changed in the concrete class (CategoryRepository), it will not break the code on the CategoryService, because this code only depends on its abstraction.
“A Dependency Injection container, sometimes, refereed as DI container or IoC container, is a framework that helps with DI. It creates and injects dependencies for us automatically.”
ASP.NET Core has a native Dependency Injection container, but you can also use another container management if you want, like Simple Injector, Autofac, and others. On this article, we are going to use the native container from .NET Core. This container is used to resolve the services when that is needed during the request process.
“ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.”
“When we register services in a container, we need to set the lifetime that we want to use. The service lifetime controls how long a result object will live for after it has been created by the container. The lifetime can be created by using the appropriate extension method on the IServiceColletion when registering the service.”
There are three lifetimes that can be used with Microsoft Dependency Injection Container, they are:
- Transient — Services are created each time they are requested. It gets a new instance of the injected object, on each request of this object. For each time you inject this object is injected in the class, it will create a new instance.
- Scoped — Services are created on each request (once per request). This is most recommended for WEB applications. So for example, if during a request you use the same dependency injection, in many places, you will use the same instance of that object, it will make reference to the same memory allocation.
- Singleton — Services are created once for the lifetime of the application. It uses the same instance for the whole application.
The dependency injection container keeps track of all instances of the created services, and they are disposed of or released for garbage collector once their lifetime has ended. This is how we can configure the DI in .NET core:
Depending on how the lifetime of an operation’s service is configured for the following interfaces, the container provides either the same or different instances of the service when requested by a class.
To have a better understanding of the difference between these three lifetimes, we will see a code that uses these three lifetimes and we will see the difference between them.
Let’s go to the code
To show how DI and how the Service Lifetimes work in practice, I’ve created a Web application with three classes and their respective interfaces and a controller. Each one of these classes will create a GUID, and the application will display in the page the GUID from each service class, this way we can visualize when each instance was created.
This is the ExampleTransientService class:
This is the ExampleScopedService class:
This is the ExampleSingletonService class:
In the Startup class there is the configuration for these three services:
In the HomeController, we are injecting these services through Dependency Injection in the constructor, and in the Index method there is the call to each service. For demonstration purpose and to make the example shorter, I’m injecting twice each one of the services, this way it will be easier to visualize what is happening:
I’ve added a breakpoint in the constructor of each service and I will explain what happens when the application is executed. This is the result on the page:
What happened is:
- The two Transient objects have two different values. It hits the constructor of ExampleTransientService class twice, once for each call.
- The two Scoped objects have the same values. It hits hit the constructor of ExampleScopedService class only once, because the request is the same.
- The two Singleton objects have the same values. It hits the constructor of ExampleSingletonService class only once.
If we refresh the page, this is the new result:
This is what happened:
- The two Transient objects again have two different values. It hits the constructor of ExampleTransientService class twice, once for each call.
- Both Scoped objects have the same value. It hits hit the constructor of ExampleScopedService class only once, because the request is the same.
- The two Singleton objects have the same values from the first time when the application runs. This second time when the page was refreshed, the did not hit the constructor of the ExampleSingletonService class, and it will not hit the constructor again unless the application is restarted.
Dependency injection makes your code cleaner, easier to maintain and easier to test. Make use of the correct lifetime can impact the performance of your application. In a short way, services that are declared as Transient will be created each time they are requested, declared as Scoped will be created on each request, and declared as Singleton will be created once and will be used the same instance for the whole lifetime of the application. I hope that with these examples it easy to see what is the difference between the lifetimes, you can also download the code and run locally and add breakpoints in the classes to see in real-time what is happening. You can check the code of this example on my GitHub:
Thanks for reading!
Software Architect’s Handbook — Joseph Ingeno