Pull to refresh
0

Условное внедрение зависимостей в ASP.NET Core. Часть 1

Reading time5 min
Views15K
Иногда возникает необходимость иметь несколько вариантов реализации определенного интерфейса и, в зависимости от определенных условий, внедрять тот или иной сервис. В этой статье мы рассмотрим варианты такого внедрения в ASP.NET Core приложении, используя встроенный Dependency Injector (DI).

В первой части статьи разберём настройку IoC-контейнера на этапе запуска приложения, с возможностью выбора одной или нескольких из имеющихся реализаций. Также рассмотрим процесс внедрения в контексте HTTP-запроса, основываясь на имеющихся в нём данных. Во второй части покажем, как можно расширить возможности DI для выбора реализации на основе текстового идентификатора сервиса.

Содержание


Часть 1. Условное получение сервиса (Conditional service resolution)
1. Environment context — условное получение сервиса в зависимости от текущей настройки Environment.
2. Configuration context — условное получение сервиса на основе файла настроек приложения.
3. HTTP request context — условное получение сервиса на основе данных веб-запроса.

Часть 2. Получение сервиса по идентификатору (Resolving service by ID)
4. Получение сервиса на основе идентификатора

1. Environment context


ASP.NET Core вводит такой механизм, как Environments.

Environment — это переменная окружения (ASPNETCORE_ENVIRONMENT), указывающая, в какой конфигурации будет выполняться приложение. ASP.NET Core по соглашению поддерживает три предопределённые конфигурации: Development, Staging и Production, но в целом имя конфигурации может быть любым.

В зависимости от установленного Environment, мы можем настраивать IoC-контейнер необходимым нам образом. Например, на этапе разработки нужно работать с локальными файлами, а на этапе тестирования и production — с файлами в облачном сервисе. Настройка контейнера в этом случае будет такой:

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


Ещё одним нововведением в ASP.NET Core стал механизм хранения пользовательских настроек, который пришёл на смену секции <appSettings/> в файле web.config. Используя файл настроек при запуске приложения, мы можем настраивать IoC-контейнер:

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>();
    }
}

Благодаря этим подходам настройка IoC-контейнера выполняется на этапе запуска приложения. Давайте теперь посмотрим, какие у нас есть возможности, если необходимо выбирать реализацию в процессе выполнения, в зависимости от параметров запроса.

3. Request context


Прежде всего, мы можем получить из IoC-контейнера все объекты, реализующие требуемый интерфейс:

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");
    }
}

Этот подход вполне решает задачу выбора реализации, однако сильно напоминает Service Locator, который уже неоднократно подвергался критике (тыц, тыц). К счастью, ASP.NET Core не оставил нас наедине с этой проблемой: если мы посмотрим на набор методов, доступных для настройки IoC-контейнера, то увидим, что что у нас есть ещё один способ решения задачи с помощью делегата:

Func<IServiceProvider, TImplementation> implementationFactory

Как вы помните, интерфейс IServiceProvider представляет собой IoC-контейнер, который мы настраиваем в методе ConfigureServices класса Startup. Кроме того, платформа ASP.NET Core также настраивает ряд собственных сервисов, которые будут нам полезны.

В рамках веб-запроса нам прежде всего пригодится сервис IHttpContextAccessor, предоставляющий объект HttpContext. С его помощью мы можем получить исчерпывающую информацию о текущем запросе, и на оснoвании этих данных выбрать нужную реализацию:

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>();
    });
}

Обратите внимание на то, что необходимо явно настроить реализацию IHttpContextAccessor. Кроме того, мы не устанавливаем типы LocalService и CloudService как реализацию интерфейса IService, а просто добавляем их в контейнер.

Благодаря доступу к HttpContext, можно использовать заголовки запроса, query string, данные формы для анализа и выбора нужной реализации:

$.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;
        });
}

И в завершение приведём ещё один пример с использованием сервиса IActionContextAccessor. Выбор реализации на основании имени экшена:

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>();
    });
}

Итак, мы рассмотрели базовые решения условной реализации, основываясь на различных данных контекста, в котором выполняется приложение. В следующей статье мы немного углубимся в возможности DI и посмотрим, каким образом можно расширить его функциональность.

Исходный код примеров можно скачать по ссылке: github.com/nix-user/AspNetCoreDI
Tags:
Hubs:
+19
Comments9

Articles

Information

Website
www.nixsolutions.com
Registered
Founded
1994
Employees
1,001–5,000 employees