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

в 8:30, , рубрики: .net, asp.net core, C#, dependency injection, Программирование

Иногда возникает необходимость иметь несколько вариантов реализации некоторого интерфейса и, в зависимости от определенных условий, производить внедрение того или другого сервиса. В данной статье будут показаны варианты такого внедрения в 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

Автор: ivanzaruba

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js