Introduction
Dependency Injection (DI) is a design pattern that supports the development of loosely coupled code, and it’s also one of the SOLID principles (Dependency Inversion Principle). In this article, I will explain how we can use Dependency Injection and what is the difference between the three service 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 its own framework brings it natively. Using Dependency Injection brings a lot of benefits.
- 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:
public class CategoryRepository : ICategoryRepository
{
public Category GetCategory()
{
return new Category();
}
}
public interface ICategoryRepository
{
Category GetCategory();
}
The interface is like a contract. So, the class that 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. In 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 the CategoryRepository class:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ICategoryRepository, CategoryRepository>();
}
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.
public class CategoryService
{
private readonly ICategoryRepository _categoryRepository;
public CategoryService(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
}
}
This constructor accepts an ICategoryRepository parameter, and it stores the dependency in a private ready-only field. Setting this property as a read-only property will avoid the possibility of other methods accidentally assigning a different value for this dependency. This class can now make use of this abstraction.
With DI, the code does not depend on a concrete class (the code does not depend 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.
DI Containers
“A Dependency Injection container, sometimes referred to as a 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. In 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.”
Service Lifetimes
“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 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. Each time you inject this object is injected into 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 by the garbage collector once their lifetime has ended. This is how we can configure the DI in the .NET core.
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IMyDependencyA, MyDependencyA>();
services.AddSingleton<IMyDependencyB, MyDependencyB>();
services.AddScoped<IMyDependencyC, MyDependencyC>();
}
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 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
using System;
using DependencyInjectionAndServiceLifetimes.Interfaces;
namespace DependencyInjectionAndServiceLifetimes.Services
{
public class ExampleTransientService : IExampleTransientService
{
private readonly Guid Id;
public ExampleTransientService()
{
Id = Guid.NewGuid();
}
public string GetGuid() => Id.ToString();
}
}
This is the ExampleScopedService class
using System;
using DependencyInjectionAndServiceLifetimes.Interfaces;
namespace DependencyInjectionAndServiceLifetimes.Services
{
public class ExampleScopedService : IExampleScopedService
{
private readonly Guid Id;
public ExampleScopedService()
{
Id = Guid.NewGuid();
}
public string GetGuid() => Id.ToString();
}
}
This is the ExampleSingletonService class
using System;
using DependencyInjectionAndServiceLifetimes.Interfaces;
namespace DependencyInjectionAndServiceLifetimes.Services
{
public class ExampleSingletonService : IExampleSingletonService
{
private readonly Guid Id;
public ExampleSingletonService()
{
Id = Guid.NewGuid();
}
public string GetGuid() => Id.ToString();
}
}
In the Startup class there is the configuration for these three services
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddTransient<IExampleTransientService, ExampleTransientService>();
services.AddScoped<IExampleScopedService, ExampleScopedService>();
services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
services.AddControllersWithViews();
}
In the HomeController, we are injecting these services through Dependency Injection in the constructor, and in the Index method, there is a call to each service. For demonstration purposes 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.
using Microsoft.AspNetCore.Mvc;
using System.Text;
using DependencyInjectionAndServiceLifetimes.Interfaces;
namespace DependencyInjectionAndServiceLifetimes.Controllers
{
public class HomeController : Controller
{
private readonly IExampleTransientService _exampleTransientService1;
private readonly IExampleTransientService _exampleTransientService2;
private readonly IExampleScopedService _exampleScopedService1;
private readonly IExampleScopedService _exampleScopedService2;
private readonly IExampleSingletonService _exampleSingletonService1;
private readonly IExampleSingletonService _exampleSingletonService2;
public HomeController(IExampleTransientService exampleTransientService1,
IExampleTransientService exampleTransientService2,
IExampleScopedService exampleScopedService1,
IExampleScopedService exampleScopedService2,
IExampleSingletonService exampleSingletonService1,
IExampleSingletonService exampleSingletonService2)
{
_exampleTransientService1 = exampleTransientService1;
_exampleTransientService2 = exampleTransientService2;
_exampleScopedService1 = exampleScopedService1;
_exampleScopedService2 = exampleScopedService2;
_exampleSingletonService1 = exampleSingletonService1;
_exampleSingletonService2 = exampleSingletonService2;
}
public IActionResult Index()
{
var exampleTransientServiceGuid1 = _exampleTransientService1.GetGuid();
var exampleTransientServiceGuid2 = _exampleTransientService2.GetGuid();
var exampleScopedServiceGuid1 = _exampleScopedService1.GetGuid();
var exampleScopedServiceGuid2 = _exampleScopedService2.GetGuid();
var exampleSingletonServiceGuid1 = _exampleSingletonService1.GetGuid();
var exampleSingletonServiceGuid2 = _exampleSingletonService2.GetGuid();
StringBuilder messages = new StringBuilder();
messages.Append($"Transient 1: {exampleTransientServiceGuid1}\n");
messages.Append($"Transient 2: {exampleTransientServiceGuid2}\n\n");
messages.Append($"Scoped 1: {exampleScopedServiceGuid1}\n");
messages.Append($"Scoped 2: {exampleScopedServiceGuid2}\n\n");
messages.Append($"Singleton 1: {exampleSingletonServiceGuid1}\n");
messages.Append($"Singleton 2: {exampleSingletonServiceGuid2}");
return Ok(messages.ToString());
}
}
}
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 the ExampleTransientService class twice, once for each call.
- The two Scoped objects have the same values. It hits the constructor of the ExampleScopedService class only once because the request is the same.
- The two Singleton objects have the same values. It hits the constructor of the 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 the ExampleTransientService class twice, once for each call.
- Both Scoped objects have the same value. It hits the constructor of the 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.
Conclusion
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 in 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 it locally and add breakpoints in the classes to see in real-time what is happening.