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

Иногда возникает необходимость иметь несколько вариантов реализации определенного интерфейса и, в зависимости от определенных условий, внедрять тот или иной сервис. В этой статье мы рассмотрим варианты такого внедрения в 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
  • +19
  • 9,4k
  • 9
NIX Solutions
70,00
Компания
Поделиться публикацией

Комментарии 9

    +1
    инъекция
    Внедрение.
    Environment это глобальная переменная, указывающая в какой конфигурации приложение будет выполняться. Таких конфигураций существует три: Development, Staging, Production.
    Не «Environment это глобальная переменная» (кто такой Environment? какая глобальная переменная?), а переменные окружения. Также, конфигурации могут существовать любые, просто приведённые три предопределены (used by convention).
      0
      инъекция

      Внедрение.

      «Инъекция» более используемый термин, поэтому написал по привычке :) Спасибо за поправку!

      Не «Environment это глобальная переменная»

      Согласен с уточнением. Я в данной статье хотел дать абстрактное понятие Environment, как некая абстрактная глобальная переменная, а для более конкретного определения дается ссылка на документацию. Спасибо большое!)
      +1
      Скажите, а чем Environments лучше Conditional compilation symbols, которые позволяют не тащить с собой ненужный в заданном окружение код?
        +1
        Это просто разные вещи. Environment определяется в момент запуска приложения.
          0
          Ок, но вопрос о преимуществах новой модели над старой.
            0
            Преимущество Environment над conditional compilation symbols, например, в том, что Envitonment определяет переменную для всего решения, в то время как символы действуют только в рамках одной сборки. Но это что касается их общего функционала, т.е. то, в чем их можно сравнить, а вообще имхо у этих механизмов разное предназначение.
              0
              А есть ли необходимость сверяться с Environment в любой точке решения? Может просто задать начальные зависимости и абстрагировать остальные компоненты?
              0
              Ок, но вопрос о преимуществах новой модели над старой.

              HostingEnvironment.IsDevelopment() фактически является заменой HttpContext.Current.IsDebuggingEnabled.
          +1
          Символы определяют, какой код будет скомпилирован, а Environment, какой будет выполнен в зависимости от значения переменной окружения.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое