Back to Home

Conditional dependency injection in ASP.NET Core. Part 1 / NIX Blog

c # · asp.net core · dependency injection

Conditional dependency injection in ASP.NET Core. Part 1

Sometimes it becomes necessary to have several options for implementing a particular interface and, depending on certain conditions, implement a particular service. In this article, we will consider 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 configuration of the IoC container at the application launch stage, with the possibility of choosing one or more of the available implementations. We also consider the implementation process in the context of an HTTP request, based on the data available in it. In the second part, we show how you can expand the capabilities of DI for choosing an implementation based on the text identifier of the service.

Content


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

Part 2. Obtaining a service by identifier (Resolving service by ID)
4. Obtaining a service based on an identifier

1. Environment context


ASP.NET Core introduces a mechanism such as Environments .

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

Depending on the installed Environment, we can configure the IoC container in the way we need. 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();
    }
    else
    {
        services.AddScoped();
    }
}

2. Configuration context


Another innovation in ASP.NET Core was the user settings storage mechanism, which replaced the section in the web.config file. Using the settings file at application launch, 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();
    }
    else if (appMode == "Cloud")
    {
        services.AddScoped();
    }
}

Thanks to these approaches, the IoC container is configured at the application launch stage. Let's now see what possibilities we have if it is necessary to choose an implementation at runtime, depending on the request parameters.

3. Request context


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

public interface IService
{
    string Name {get; set; }
}
public class LocalController
{
    private readonly IService service;
    public LocalController(IEnumerable services)
    {
        // из всех реализаций выбираем необходимую
        this.service = services.FirstOrDefault(svc => svc.Name == "local");
    }
}

This approach completely solves the problem of choosing an implementation, but it closely resembles the Service Locator, which has 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 to configure the IoC container, we will see that we have another way to solve the problem using the delegate:

Func implementationFactory

As you remember, the interface IServiceProvideris an IoC container that we configure in the ConfigureServicesclass method Startup. In addition, the ASP.NET Core platform also sets up a number of its own services that will be useful to us.

As part of the web request IHttpContextAccessor, the service that provides the object is primarily useful to us HttpContext. 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();
    services.AddScoped();
    services.AddScoped();
    services.AddScoped(serviceProvider => {
        var httpContext = serviceProvider.GetRequiredService();
        return httpContext.IsLocalRequest() // IsLocalRequest() is a custom extension method, not a part of ASP.NET Core
            ? serviceProvider.GetService()
            : serviceProvider.GetService();
    });
}

Note that you must explicitly configure the implementation IHttpContextAccessor. In addition, we do not set the types LocalServiceand CloudServicehow implementation of the interface IService, and 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();
    services.AddScoped();
    services.AddScoped();
    services.AddScoped(serviceProvider => {
        var httpContext = serviceProvider.GetRequiredService().HttpContext;
        if (httpContext == null)
        {
            // Разрешение сервиса происходит не в рамках HTTP запроса
            return null;
        }
        // Можно использовать любые данные запроса
        var queryString = httpContext.Request.Query;
        var requestHeaders = httpContext.Request.Headers;
        return requestHeaders.ContainsKey("Use-local")
            ? serviceProvider.GetService() as IService
            : serviceProvider.GetService() as IService;
        });
}

And finally, we give another example using the service IActionContextAccessor. Choosing an implementation based on the action name:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped();
    services.AddScoped();
    services.AddScoped();
    services.AddScoped();
    services.AddScoped(serviceProvider => {
        var actionName = serviceProvider.GetRequiredService().ActionContext?.ActionDescriptor.Name;
        // Если имя экшена отсутствует, значит разрешение сервиса происходит не в рамках веб-запроса, а, например, в классе Startup
        if (actionName == null) return ResolveOutOfWebRequest(serviceProvider);
        return actionName == "UseLocalService" 
            ? serviceProvider.GetService()
            : serviceProvider.GetService();
    });
}

So, we examined the basic solutions of a conditional implementation, based on various data from the context in which the application is running. In the next article, we will go a little deeper into the capabilities of DI and see how to extend its functionality.

The source code for the examples can be downloaded at: github.com/nix-user/AspNetCoreDI

Read Next