Index
In this post, we are going to see what dependency injection is, mainly in a web environment, although as we saw in the post on dependency inversion it can also be used in a console application.
We will look at why to use dependency injection, the different types or options we have when creating our dependency container: scoped, singleton, and transient,
And finally, an example in C#.
Dependency injection is commonly referred to as DI, which comes from its initials in English for Dependency Injection.
1 - Why Use Dependency Injection
We need to understand that dependency injection is a design pattern; using this pattern ties together two concepts: the first, which we have already seen, is the SOLID principle of dependency inversion along with Inversion of Control.
ASP.NET Core was designed from the beginning for us to develop our code using dependency injection, and the entire framework uses it.
To understand what dependency injection is, we must first understand what a dependency is.
1.1 - What is a Dependency
Following the example in this course, we have a class that collects the personal profile.
We have code that retrieves information either from the database or from a file system, groups it, and displays it.
The action of retrieving from the database is what we call a dependency.
public PersonalProfileDto GetPersonalProfileDto(int userId)
{
var personalProfileInfo = new PersonalProfileInfo();
var pefil = personalProfileInfo.GetPerfil(userId);
var skills = personalProfileInfo.GetSkills(userId);
var intereses = personalProfileInfo.GetIntereses(userId);
return Map(peril, skills, intereses);
}
Currently our method GetPersonalProfileDto
is responsible for creating the instance of the profile dependency.
1.2 - Problems Instantiating Dependencies
First, since the method is responsible for creating the instance, it is strongly tied to our current implementation.
This problem is always linked to changes in the future, because if we want to change, for example, to obtain the data from a public API instead of the database, we would also have to change all the implementations of new PersonalProfileInfo();
to new PersonalProfileInfoAPI();
This example makes our code much harder to maintain.
Another problem is when we want to do unit testing. Our methods have logic that we want to test. But to test this small method, we would also have to implement everything that the PersonalProfileInfo
instance needs and make it return the information we need in the methods we call.
For this reason, testing when we have to instantiate dependencies is almost impossible to test properly.
2 - Using Interfaces in Dependency Injection
The SOLID principle of dependency inversion tells us that our classes should depend on abstractions, not on implementations.
But what does that mean? Currently, our method is tied to the implementation of PersonalProfileInfo
. To abstract our code, we need to extract an interface and our class PersonalProfileInfo
must implement this interface.
The interface will contain all the methods we need to build the personal profile.
public interface IPersonalProfileInfo
{
List<InterestEntity> GetInterests(int userId);
List<SkillEntity> GetSkills(int iduserId);
PersonalProfileEntity GetPerfil(int userId);
}
public class PersonalProfileInfo : IPersonalProfileInfo
{
//code
}
Thanks to this implementation of the interface, we will be able to create unit tests containing dependencies because we can do a mock of them.
3 - Constructor Dependency Injection
Our code change does not end here because we have not changed our main method.
For our next step, we are going to talk about constructor dependency injection, or “constructor injection” in English.
In the class or service that contains the GetPersonalProfileDto
method, we need to create a constructor, and this constructor must receive as a parameter the interface we just created.
We must assign the parameter to a property of the class, which should be immutable to prevent the property from being modified.
Finally, in our GetPersonalProfileDto
method, we will no longer use the new PersonalProfileInfo()
instance, but instead use the property to which we've just assigned the value from the constructor.
public class PerilPersonal
{
private readonly IPersonalProfileInfo _personalProfileInfoDependency;
public PerfilPersonal(IPersonalProfileInfo personalProfileInfo)
{
_personalProfileInfo = personalProfileInfo;
}
public PersonalProfileDto GetPersonalProfileDto(int userId)
{
var pefil = _personalProfileInfoDependency.GetPerfil(userId);
var skills = _personalProfileInfoDependency.GetSkills(userId);
var intereses = _personalProfileInfoDependency.GetIntereses(userId);
return Map(peril, skills, intereses);
}
}
4 - Registering Our Dependencies with the Dependency Container
To use dependency injection, we must specify which class each interface we are injecting refers to.
We need to configure the dependency injection container when the application starts running.
In ASP.NET Core we use IServiceCollection
to register our services (dependencies). Any service you want to inject must be registered in the IServiceCollection
container and these will be resolved by IServiceProvider
once our IServiceCollection
has been built.
It may sound strange, but it’s really simple. Let’s get to it;
In ASP.NET Core, the most common place to register the container is in the ConfigureServices
method of the startup
class.
To register our services (dependencies), we have several extension methods that allow us to do so.
For this, we have three options: Scoped
, Transient
, and Singleton
. We’ll see their differences in the next section.
All three options register our service in the dependency container.
They have the same structure as they accept two generic arguments.
services.AddTransient<IPersonalProfileInfo, PersonalProfileInfo>();
services.AddScoped<IPersonalProfileInfo, PersonalProfileInfo>();
services.AddSingleton<IPersonalProfileInfo, PersonalProfileInfo>();
- The first argument is our abstraction, meaning the interface we will use for injecting in the different constructors.
- The second element is the implementation that uses the abstraction.
If we do NOT want to introduce an abstraction, we can indicate the class directly.
services.AddTransient<PersonalProfileInfo>();
Types of Dependency Injection
When we register services in our container, we have three options as explained above. These three options only differ in one thing: the lifetime.
The container keeps all the services it creates, and once their lifetime ends, they are disposed or released for the garbage collector.
It is important to choose the right lifetime, as not all will give us the same results.
For this example we have a small service that simply generates a GUID each time it is instantiated, along with a small controller and a middleware.
public class GuidService
{
public readonly Guid ResultadoGuid;
public GuidService()
{
ResultadoGuid = Guid.NewGuid();
}
}
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
private readonly GuidService _guidService;
private readonly ILogger<HomeController> _logger;
public HomeController(GuidService guidService, ILogger<HomeController> logger)
{
_guidService = guidService;
_logger = logger;
}
[HttpGet]
public IActionResult Index()
{
var logMessage = $"Controller:{_guidService.ResultadoGuid}";
_logger.LogInformation(logMessage);
return Ok();
}
}
public class EjMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<EjMiddleware> _logger;
public EjMiddleware(RequestDelegate next, ILogger<EjMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, GuidService guidService)
{
var logMessage = $"Middleware: {guidService.ResultadoGuid}";
_logger.LogInformation(logMessage);
await _next(context);
}
}
The application can be downloaded from GitHub
4.1 - When to Use Transient
When a service is registered as transient, it means an instance will be created each time the dependency is resolved.
In other words, each time we use a "Transient" service, it will be a new instance:
- We use Transient when the service contains state that may change (mutable) and is not thread-safe. Since each service receives its own instance, we can consume the methods without worrying about other services accessing them.
- On the other hand, we use more memory since we will require more objects, making it less efficient.
- It is very common to use transient services when it’s not clear which option to use.
This would be the result of running the example code
[
"Middleware: 1b3051c7-1ea0-481d-8edf-18d3c9ef6496",
"Controller: 03b111f2-62b7-4996-8cb6-bb83f782403b"
]
As we can see, for each of the classes a new instance was created.
4.2 - When to Use Scoped
If we create a service that is registered as scoped, it will live for the lifetime of that scope.
In ASP.NET Core, the application creates a scope for each request it receives, usually from the browser.
Each scoped service will be created once per request.
This means that all services will receive the same instance throughout the scope (the request):
Request 1:
[
"Middleware: 0cff9660-1b62-4475-a1ae-976d9516abf9",
"Controller: 0cff9660-1b62-4475-a1ae-976d9516abf9"
]
Request 2:
[
"Middleware: 52dd3c3f-435e-4d72-99a3-00e227442ef4",
"Controller: 52dd3c3f-435e-4d72-99a3-00e227442ef4"
]
A very common example of creating a scoped service is when we have a "logId
" to be able to map all the points through which the request has passed.
4.3 - When to Use Singleton
When a service is registered as a singleton, it means it will be created only once while the application is active.
The same instance will be reused and injected into all classes that use it.
If the service is used very frequently, it can improve performance because it is instantiated once for the entire application, and not once per service as in Transient.
If we use singleton, we must make the referenced class either completely immutable, since as it is the same instance, if it’s mutable, it could cause unexpected errors.
Or use techniques to ensure that the application will be thread-safe. A very common example of singleton is in-memory cache.
Request 1:
[
"Middleware: 01ae4b92-18ab-457a-be01-9a47ac231915",
"Controller: 01ae4b92-18ab-457a-be01-9a47ac231915"
]
Request 2:
[
"Middleware: 01ae4b92-18ab-457a-be01-9a47ac231915",
"Controller: 01ae4b92-18ab-457a-be01-9a47ac231915"
]
Finally, when using singleton, we must be careful because a single instance can mean a lot of memory that will never be released by the garbage collector. We must take this into account.
4.4 - Scope Validation
When programming, defining the type of lifetime we will give to our services is important, but not only that; we must also ensure that our services do not depend on other services with a shorter lifetime than themselves.
This is to avoid errors or unexpected behaviors during runtime.
Since ASP.NET Core 2.0, the framework itself comes with a validation system that will check that we are adding all the scopes correctly.
Conclusion
Dependency injection allows us to create applications that are much easier to maintain and work with.
Moreover, thanks to abstractions, it allows us to create interfaces that help with testing.
The code is much cleaner and more readable.
If there is any problem you can add a comment bellow or contact me in the website's contact form