[Из песочницы] Условная инъекция зависимостей в ASP.NET Core. Часть 1

Иногда возникает необходимость иметь несколько вариантов реализации некоторого интерфейса и, в зависимости от определенных условий, производить инъекцию того или другого сервиса. В данной статье будут показаны варианты такой инъекции в 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 это глобальная переменная, указывающая в какой конфигурации приложение будет выполняться. Таких конфигураций существует три: Development, Staging, Production.

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

public IHostingEnvironment HostingEnvironment { get; }

public void ConfigureServices(IServiceCollection services)
{
    if (this.HostingEnvironment.IsDevelopment())
    {
        services.AddScoped();
    }
    else
    {
        services.AddScoped();
    }
}

2. Configuration context


Еще одним нововведением в ASP.NET Core стал механизм хранения пользовательских настроек, который пришел на замену секции в файле 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();
    }
    else if (appMode == "Cloud")
    {
        services.AddScoped();
    }
}

Используя такие подходы, настройка 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 services = serviceProvider.GetServices();
        // из всех реализаций выбираем необходимую
        this.service = services.FirstOrDefault(svc => svc.Name == "local");
    }
}

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

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

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

Если мы посмотрим на набор методов, который предоставляет ASP.NET Core для настройки IoC-контейнера, становится очевидно, что использовать лучше всего те методы, где у нас есть возможность повлиять на логику создания объекта, благодаря делегату:

Func implementationFactory

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

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

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

Обратите внимание на то, что необходимо явно настроить реализацию 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();
    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;
        });
}

И в завершение еще один пример с использованием сервиса IActionContextAccessor. Выбор реализации на основании имени экшена:
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();
    });
}

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

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

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

  • 3 августа 2016 в 12:46

    0

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

© Habrahabr.ru