📜 ⬆️ ⬇️

Conditional dependency injection in ASP.NET Core. Part 1

Sometimes it becomes necessary to have several options for implementing a specific interface and, depending on certain conditions, implement a particular service. In this article we will look at options for such an implementation in an ASP.NET Core application using the built-in Dependency Injector (DI).

In the first part of the article we will analyze the setting of the IoC container at the stage of launching the application, with the possibility of choosing one or several of the available implementations. We will also consider the implementation process in the context of an HTTP request, based on the data it contains. In the second part, we show how you can extend the capabilities of the DI to select an implementation based on the text identifier of the service.

Content


Part 1. Conditional service resolution
1. Environment context - conditional receipt of the service depending on the current Environment setting.
2. Configuration context - conditional receipt of the service based on the application settings file.
3. HTTP request context - conditional receipt of the service based on the web request data.

Part 2. Getting service by ID (Resolving service by ID)
4. Getting service based on ID
')

1. Environment context


ASP.NET Core introduces a mechanism such as Environments .

Environment is an environment variable (ASPNETCORE_ENVIRONMENT) indicating the configuration in which the application will run. ASP.NET Core by convention supports three predefined configurations: Development, Staging and Production, but in general the configuration name can be anything.

Depending on the Environment installed, we can configure the IoC container as needed. For example, at the development stage, you need to work with local files, and at the testing and production stage, with files in the cloud service. The container setting in this case will be as follows:

public IHostingEnvironment HostingEnvironment { get; } public void ConfigureServices(IServiceCollection services) { if (this.HostingEnvironment.IsDevelopment()) { services.AddScoped<IFileSystemService, LocalhostFileSystemService>(); } else { services.AddScoped<IFileSystemService, AzureFileSystemService>(); } } 

2. Configuration context


Another new feature in ASP.NET Core is the user settings storage mechanism, which replaced the <appSettings /> section in the web.config file. Using the settings file when launching the application, we can configure the IoC container:

 appsettings.json { "ApplicationMode": "Cloud" // Cloud | Localhost } 

 public void ConfigureServices(IServiceCollection services) { var appMode = this.Configuration.GetSection("ApplicationMode").Value; if (appMode == "Localhost") { services.AddScoped<IService, LocalhostService>(); } else if (appMode == "Cloud") { services.AddScoped<IService, CloudService>(); } } 

Thanks to these approaches, IoC container configuration is performed at the stage of launching the application. Let's now see what opportunities we have if we need to choose an implementation during the execution, depending on the parameters of the request.

3. Request context


First of all, we can get from the IoC container all objects that implement the required interface:

 public interface IService { string Name {get; set; } } public class LocalController { private readonly IService service; public LocalController(IEnumerable<IService> services) { //      this.service = services.FirstOrDefault(svc => svc.Name == "local"); } } 

This approach completely solves the problem of choosing an implementation, but strongly reminds Service Locator, which has already been criticized several times ( tyts , tyts ). Fortunately, ASP.NET Core did not leave us alone with this problem: if we look at the set of methods available for configuring the IoC container, we will see that we have another way to solve the problem using a delegate:

 Func<IServiceProvider, TImplementation> implementationFactory 

As you remember, the IServiceProvider interface is an IoC container, which we configure in the ConfigureServices method of the Startup class. In addition, the ASP.NET Core platform also configures a number of proprietary services that will be useful to us.

As part of a web request, we first of all come in handy with the IHttpContextAccessor service, which provides an HttpContext object. With it, we can get comprehensive information about the current request, and based on this data, select the desired implementation:

 public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<LocalService>(); services.AddScoped<CloudService>(); services.AddScoped<IService>(serviceProvider => { var httpContext = serviceProvider.GetRequiredService<IHttpContextAccessor>(); return httpContext.IsLocalRequest() // IsLocalRequest() is a custom extension method, not a part of ASP.NET Core ? serviceProvider.GetService<LocalService>() : serviceProvider.GetService<CloudService>(); }); } 

Note that you need to explicitly configure the IHttpContextAccessor implementation. In addition, we do not set the LocalService and CloudService as an implementation of the IService interface, but simply add them to the container.

Thanks to access to HttpContext , you can use the request headers, query string, form data to analyze and select the desired implementation:

 $.ajax({ type:"POST", beforeSend: function (request) { request.setRequestHeader("Use-local", "true"); }, url: "UseService", data: { id = 100 }, }); 

 public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<LocalService>(); services.AddScoped<CloudService>(); services.AddScoped(serviceProvider => { var httpContext = serviceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext; if (httpContext == null) { //       HTTP  return null; } //      var queryString = httpContext.Request.Query; var requestHeaders = httpContext.Request.Headers; return requestHeaders.ContainsKey("Use-local") ? serviceProvider.GetService<LocalhostService>() as IService : serviceProvider.GetService<CloudService>() as IService; }); } 

And in conclusion we will give one more example using the IActionContextAccessor service. The choice of implementation based on the name of the action:

 public void ConfigureServices(IServiceCollection services) { services.AddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<IActionContextAccessor, ActionContextAccessor>(); services.AddScoped<LocalService>(); services.AddScoped<CloudService>(); services.AddScoped<IService>(serviceProvider => { var actionName = serviceProvider.GetRequiredService<IActionContextAccessor>().ActionContext?.ActionDescriptor.Name; //    ,        -, , ,   Startup if (actionName == null) return ResolveOutOfWebRequest(serviceProvider); return actionName == "UseLocalService" ? serviceProvider.GetService<LocalService>() : serviceProvider.GetService<CloudService>(); }); } 

So, we looked at the basic solutions of the conditional implementation, based on the various data of the context in which the application runs. In the next article, we delve a little into the possibilities of DI and see how we can extend its functionality.

The source code of the examples can be downloaded from the link: github.com/nix-user/AspNetCoreDI

Source: https://habr.com/ru/post/306996/


All Articles