There is a video version of this tutorial:
Since its beginnings, ASP.NET Core has come with a dependency injection system. With this system we can centralize the mechanism that provides the different dependencies of our classes in one place. Using dependency injection is a good practice that allows us to apply the dependency inversion principle to have flexible software.
In more concrete terms, when we use the dependency injection system we configure what class to serve when a particular service is requested. For example:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services.AddScoped<IFileStorageService, AzureStorageService>(); |
Note: In this entry we are using AddScoped, however, everything we will learn applies to both AddTransient and AddSingleton.
The previous code means that when a class requests the IFileStorageService service, then an instance of the AzureStorageService class must be delivered. That is, if we have a class called PeopleController, which requests the IFileStorageService service through the constructor, then, at runtime, an instance of the AzureStorageService class will be served:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[ApiController] | |
[Route("api/[controller]")] | |
public class PeopleController : ControllerBase | |
{ | |
private readonly IFileStorageService fileStorageService; | |
public PeopleController(IFileStorageService fileStorageService) | |
{ | |
this.fileStorageService = fileStorageService; | |
} | |
// … | |
} |
However, the way we are using the dependency injection system is not very flexible.
Suppose we have another class that implements the IFileStorageService interface and we need the following: When we are in a development environment, we want the InAppStorageService class to be served, and, when we are in a non-development environment, we want to use AzureStorageService. How can we add this flexibility? We can use a factory.
When we talk about factory we refer to a mechanism within your software which is responsible for instantiating classes and returning those instances.
The ASP.NET Core dependency injection system allows us to define our own factories in order to add custom logic when selecting the class we wish to serve when supplying a service.
If we look at the AddScoped overloads, we will see that one of them has the following signature:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static IServiceCollection AddScoped<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class; |
As we can see, the previous overload indicates a parameter of type Func, which sends as a parameter an IServiceProvider, and returns a TService.
In Plain English, the above means that we can send a factory method which will return a class that implements our IFileStorageService interface. In addition, we can have a service provider for the case in which we need to use a service within our factory. This is exactly what we need.
Let’s see an implementation. Notice that we remove the type argument AzureStorageService, because now we have two implementations to use. In addition, we use the IServiceProvider to obtain an instance of IWebHostEnvironment, which is a service that helps us determine what environment we are in:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
services.AddScoped<IFileStorageService>((serviceProvider) => | |
{ | |
var env = serviceProvider.GetRequiredService<IWebHostEnvironment>(); | |
var configuration = serviceProvider.GetRequiredService<IConfiguration>(); | |
if (env.IsDevelopment()) | |
{ | |
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); | |
return new InAppStorageService(env, httpContextAccessor); | |
} | |
else | |
{ | |
return new AzureStorageService(configuration); | |
} | |
}); |
In this way, one class or another will be used depending on the environment in which we are executing our application.
If you don’t like to be placing the factory directly in the AddScoped method, you can put it in a class. In my case I am going to use a static class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class Factories | |
{ | |
public static IFileStorageService FileStorageService(IServiceProvider serviceProvider) | |
{ | |
var env = serviceProvider.GetRequiredService<IWebHostEnvironment>(); | |
var configuration = serviceProvider.GetRequiredService<IConfiguration>(); | |
if (env.IsDevelopment()) | |
{ | |
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); | |
return new InAppStorageService(env, httpContextAccessor); | |
} | |
else | |
{ | |
return new AzureStorageService(configuration); | |
} | |
} | |
} |
Then, in the AddScoped method we can choose one of the following two options:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Option 1 | |
services.AddScoped(Factories.FileStorageService); | |
// Option 2 | |
services.AddScoped<IFileStorageService>(Factories.FileStorageService); |
Both options do the same, though option 2 is explicit in regard to the service to be configured.
Summary
- ASP.NET Core has a dependency injection system integrated
- We can use factories to customize the logic of selecting service implementations
Regards!