Иногда возникает необходимость иметь несколько вариантов реализации некоторого интерфейса и, в зависимости от определенных условий, производить внедрение того или другого сервиса. В данной статье будут показаны варианты такого внедрения в ASP.NET Core приложении, используя встроенный инъектор зависимостей.

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

Содержание


Часть 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), указывающая в какой конфигурации приложение будет выполняться. Существует три предопределенных конфигурации: Development, Staging, Production, поддерживаемых ASP.NET Core по соглашению, но в целом имя конфигурации может быть любым.

В зависимости от установленного 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-контейнера все объекты, реализующие требуемый интерфейс. Для этого, мы делаем внедрение непосредственно IoC-контейнера, который в ASP.NET Core представлен интерфейсом System.IServiceProvider

public interface IService
{
    string Name {get; set; }
}

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

Интерфейс IServiceProvider можно внедрять не только в конструкторы, но и в экшены контроллера, используя атрибут [FromServices]. Также этот интерфейс можно внедрять в конструкторы классов из других сборок. Чтобы интерфейс IServiceProvider был внедрен в такой класс, нужно сам класс другой сборки добавить в контейнер и получить экземпляр этого класса при помощи внедрения или через метод GetService контейнера.

Такой подход будет оптимальным, если объекты, реализующие IService, не будут слишком «тяжелыми», т.е. не будут содержать длинный граф зависимостей либо будут объектами Singleton.

Но скорее всего, создавать все объекты будет затратно, поэтому рассмотрим, какие еще средства у нас есть при настройке IoC-контейнера.

Если мы посмотрим на набор методов, который предоставляет 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>();
    });
}

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

Исходный код примеров можно скачать по ссылке: github.com/izaruba/AspNetCoreDI
Поделиться с друзьями
-->

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


  1. withkittens
    03.08.2016 12:46
    +1

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


    1. ivanzaruba
      03.08.2016 15:32

      инъекция

      Внедрение.

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

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

      Согласен с уточнением. Я в данной статье хотел дать абстрактное понятие Environment, как некая абстрактная глобальная переменная, а для более конкретного определения дается ссылка на документацию. Спасибо большое!)


  1. osmirnov
    03.08.2016 17:32
    +1

    Скажите, а чем Environments лучше Conditional compilation symbols, которые позволяют не тащить с собой ненужный в заданном окружение код?


    1. dimkname
      03.08.2016 18:36
      +1

      Это просто разные вещи. Environment определяется в момент запуска приложения.


      1. osmirnov
        03.08.2016 19:37

        Ок, но вопрос о преимуществах новой модели над старой.


        1. ivanzaruba
          03.08.2016 22:16

          Преимущество Environment над conditional compilation symbols, например, в том, что Envitonment определяет переменную для всего решения, в то время как символы действуют только в рамках одной сборки. Но это что касается их общего функционала, т.е. то, в чем их можно сравнить, а вообще имхо у этих механизмов разное предназначение.


          1. osmirnov
            03.08.2016 22:27

            А есть ли необходимость сверяться с Environment в любой точке решения? Может просто задать начальные зависимости и абстрагировать остальные компоненты?


        1. Taritsyn
          07.08.2016 22:10

          Ок, но вопрос о преимуществах новой модели над старой.

          HostingEnvironment.IsDevelopment() фактически является заменой HttpContext.Current.IsDebuggingEnabled.


  1. ivanzaruba
    03.08.2016 22:36
    +1

    Символы определяют, какой код будет скомпилирован, а Environment, какой будет выполнен в зависимости от значения переменной окружения.