[Из песочницы] Использование единого IoC Container'a в рамках HTTP-запроса между Web API и OWIN Middleware

Целью данной статьи является поиск рабочего решение, которое позволяет иметь единый контейнер зависимостей (IoC контейнер) на протяжении всего жизненного цикла запроса, контролировать его создание и уничтожение.

image

Это может понадобиться в том случае, если web-приложение должно иметь транзакционность (а на мой взгляд любое web-приложение его обязано иметь, т.е. применять изменения (например в БД) только в случае успешной обработки запроса и делать их отмену, если на любом из этапов возникла ошибка, свидетельствующая о некорректном результате и неконтролируемых последствиях) (github source code).

Теория


Проекты Web API 2 конфигурируются с помощью OWIN интерфейса IAppBuilder, который призван помочь построить pipeline обработки входящего запроса.

На изображении выше виден жизненный цикл запроса,- он проходит по всем компонентам цепочки, затем попадает в Web API (что также является отдельным компонентом) и возвращается обратно, формируя или декорируя ответ от сервера.

Для того, чтобы иметь единый контейнер зависимостей нам потребуется создавать его явно в начале обработки запроса и уничтожать по завершению:

  1. Начало обработки запроса;
  2. Создание контейнера;
  3. Использование контейнера в Middleware;
  4. Использование контейнера в Web API;
  5. Уничтожение контейнера;
  6. Завершение обработки запроса.

Для этого нам достаточно сконфигурировать контейнер, зарегистрировать его в Web API (посредством DependencyResolver):
// Configure our parent container
var container = UnityConfig.GetConfiguredContainer();
            
// Pass our parent container to HttpConfiguration (Web API)
var config = new HttpConfiguration {
    DependencyResolver = new UnityDependencyResolver(container)
};

WebApiConfig.Register(config);

Написать собственный Middleware, который будет создавать дочерний контейнер:
public class UnityContainerPerRequestMiddleware : OwinMiddleware
{
    public UnityContainerPerRequestMiddleware(OwinMiddleware next, IUnityContainer container) 
       : base(next)
    {
        _next = next;
        _container = container;
    }

    public override async Task Invoke(IOwinContext context)
    {
        // Create child container (whose parent is global container)
        var childContainer = _container.CreateChildContainer();

        // Set created container to owinContext 
        // (to become available at other places using OwinContext.Get(key))
        context.Set(HttpApplicationKey.OwinPerRequestUnityContainerKey, childContainer);

        await _next.Invoke(context);

        // Dispose container that would dispose each of container's registered service
        childContainer.Dispose();
    }

    private readonly OwinMiddleware _next;
    private readonly IUnityContainer _container;
}

И использовать его в других Middleware«ах (в моей реализации я сохраняю контейнер в глобальном OwinContext с помощью context.Set, который передаётся в каждый следующий middleware и получаю его с помощью context.Get):
public class CustomMiddleware : OwinMiddleware
{
    public CustomMiddleware(OwinMiddleware next) : base(next)
    {
        _next = next;
    }

    public override async Task Invoke(IOwinContext context)
    {
        // Get container that we set to OwinContext using common key
        var container = context.Get(
                HttpApplicationKey.OwinPerRequestUnityContainerKey);

        // Resolve registered services
        var sameInARequest = container.Resolve();

        await _next.Invoke(context);
    }

    private readonly OwinMiddleware _next;
}

На этом можно было бы закончить, если бы не одно НО.

Проблема


Middleware Web API внутри себя имеет свой собственный цикл обработки запроса, который выглядит следующим образом:
  1. Запрос попадает в HttpServer для начала обработки HttpRequestMessage и передачи его в систему маршрутизации;
  2. HttpRoutingDispatcher извлекает данные из запроса и с помощью таблицы Route«ов определяет контроллер, ответственный за обработку;
  3. В HttpControllerDispatcher создаётся определённый ранее контроллер и вызывается метод обработки запроса с целью формирования HttpResponseMessage.

За создание контроллера отвечает следующая строка в DefaultHttpControllerActivator:
IHttpController instance = (IHttpController)request.GetDependencyScope().GetService(controllerType);

Основное содержимое метода GetDependencyScope:
public static IDependencyScope GetDependencyScope(this HttpRequestMessage request) {
    // …

    IDependencyResolver dependencyResolver = request.GetConfiguration().DependencyResolver;
    result = dependencyResolver.BeginScope();

    request.Properties[HttpPropertyKeys.DependencyScope] = result;
    request.RegisterForDispose(result);    

    return result;
}

Из него видно, что Web API запрашивает DependencyResolver, который мы для него зарегистрировали в HttpConfiguration и с помощью dependencyResolver.BeginScope () создаёт дочерний контейнер, в рамках которого уже и будет создан экземпляр ответственного за обработку запроса контроллера.

Для нас это значит следующее: контейнер, который мы используем в наших Middleware«ах и в Web API не являются одними и теми же,- больше того, они находятся на одном уровне вложенности, где глобальный контейнер — их общий родитель, т.е.:

  1. Глобальный контейнер;
    1. Дочерний контейнер, созданный в UnityContainerPerRequestMiddleware;
    2. Дочерний контейнер, созданный в Web API.

Для Web API это выглядит вполне логичным в том случае, когда оно является единственным местом обработки запроса,- контейнер создается вначале и уничтожается в конце (это ровно то, чего мы стараемся добиться).

Однако, в данный момент Web API является лишь одним из звеньев в pipeline, а значит от создания собственного контейнера придется отказаться,- нашей задачей является переопределить данное поведение и указать контейнер, в рамках которого Web API требуется создавать контроллеры и Resolve«ить зависимости.

Решение


Для решения выше поставленной проблемы мы можем реализовать собственный IHttpControllerActivator, в методе Create которого будем получать созданный ранее контейнер и уже в рамках него Resolve«ить зависимости:
public class ControllerActivator : IHttpControllerActivator
{
    public IHttpController Create(
        HttpRequestMessage request,
        HttpControllerDescriptor controllerDescriptor,
        Type controllerType
    )
    {
        // Get container that we set to OwinContext using common key
        var container = request.GetOwinContext().Get(
                HttpApplicationKey.OwinPerRequestUnityContainerKey);

        // Resolve requested IHttpController using current container
        // prevent DefaultControllerActivator's behaviour of creating child containers 
        var controller = (IHttpController)container.Resolve(controllerType);

        // Dispose container that would dispose each of container's registered service
        // Two ways of disposing container:
        // 1. At UnityContainerPerRequestMiddleware, after owin pipeline finished (WebAPI is just a part of pipeline)
        // 2. Here, after web api pipeline finished (if you do not use container at other middlewares) (uncomment next line)
        // request.RegisterForDispose(new Release(() => container.Dispose()));

        return controller;
    }
}

Для того, чтобы использовать его в Web API всё что нам остаётся, это заменить стандартный HttpControllerActivator в конфигурации:
var config = new HttpConfiguration {
    DependencyResolver = new UnityDependencyResolver(container)
};

// Use our own IHttpControllerActivator implementation 
// (to prevent DefaultControllerActivator's behaviour of creating child containers per request)
config.Services.Replace(typeof(IHttpControllerActivator), new ControllerActivator());

WebApiConfig.Register(config);

Таким образом, мы получаем следующий механизм работы с нашим единым контейнером:

1. Начало обработки запроса;

2. Создание дочернего контейнера от глобального;

var childContainer = _container.CreateChildContainer();

3. Присваивание контейнера в OwinContext:
context.Set(HttpApplicationKey.OwinPerRequestUnityContainerKey, childContainer);

4. Использование контейнера в других Middleware«ах;
var container = context.Get(HttpApplicationKey.OwinPerRequestUnityContainerKey);

5. Использование контейнера в Web API;

5.1. Получение контроллера из OwinContext:

var container = request.GetOwinContext().Get(HttpApplicationKey.OwinPerRequestUnityContainerKey);

5.2. Создание контроллера на основе этого контейнера:
var controller = (IHttpController)container.Resolve(controllerType);

6. Уничтожение контейнера:
childContainer.Dispose();

7. Завершение обработки запроса.

Результат


Конфигурируем зависимости в соответствии с требуемыми нам их жизненными циклами:
public static void RegisterTypes(IUnityContainer container)
{
    // ContainerControlledLifetimeManager - singleton's lifetime
    container.RegisterType(new ContainerControlledLifetimeManager());
    container.RegisterType(new ContainerControlledLifetimeManager());

    // HierarchicalLifetimeManager - container's lifetime
    container.RegisterType(new HierarchicalLifetimeManager());

    // TransientLifetimeManager (RegisterType's default) - no lifetime
    container.RegisterType(new TransientLifetimeManager());
}

  1. ContainerControlledLifetimeManager — создание единственного экземпляра в рамках приложения;
  2. HierarchicalLifetimeManager — создание единственного экземпляра в рамках контейнера (где мы добились того, что контейнер единый в рамках HTTP запроса);
  3. TransientLifetimeManager — создание экземпляра при каждом обращении (Resolve).

image

В изображении выше отображены GetHashCode«ы зависимостей в разрезе нескольких HTTP запросов, где:
  1. AlwaysTheSame — singleton объект в рамках приложения;
  2. SameInARequest — singleton объект в рамках запроса;
  3. AlwaysDifferent — новый экземпляр для каждого Resolve.

» Исходники доступны на github.

Материалы:

1. Конвейер в ASP.NET Web API

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

  • 28 сентября 2016 в 22:17

    0

    Может мне кто-нибудь сказать в каких сценариях нельзя реализовать хранение per request экземпляров без заморочек с IoC контейнерами?

    Почему контроль жизненного цикла должен быть настолько глубоко запрятан?

    Мы просто отдаем дань «новомодным веяниям»?

    • 28 сентября 2016 в 22:20 (комментарий был изменён)

      0

      В данном случае Web API просто встроили в OWIN’овский pipeline,- жизненный цикл запроса же в Web API оставили без изменений, что вполне корректно для случаев, когда Web API является единственным Middleware (или конфигурируется без IAppBuilder, а с помощью global.asax, как и есть в большинстве случаев).
    • 28 сентября 2016 в 22:50

      0

      Может мне кто-нибудь сказать в каких сценариях нельзя реализовать хранение per request экземпляров без заморочек с IoC контейнерами?

      Per-request-экземпляров чего?


      Почему контроль жизненного цикла должен быть настолько глубоко запрятан?

      А где он глубоко запрятан-то? В OWIN он вообще на поверхности, в WebAPI тоже далеко ходить не надо.

  • 28 сентября 2016 в 22:58

    0

    Использование единого IoC Container’a в рамках HTTP-запроса между Web API и OWIN Middleware

    Пропущено ключевое слово: Unity. Потому что для Autofac, например, весь поиск рабочего решения сводится к статье из документации (спойлер: одна дополнительная строчка по сравнению с конфигурацией для OWIN, или конфигурацией для WebAPI).


    на мой взгляд любое web-приложение его [транзакционность] обязано иметь, т.е. применять изменения (например в БД) только в случае успешной обработки запроса и делать их отмену, если на любом из этапов возникла ошибка, свидетельствующая о некорректном результате и неконтролируемых последствиях

    Ну, а вот это как раз очень спорное утверждение: во-первых, ACID-транзакция вообще не всегда обязательна, а во-вторых, она уж точно не обязательна в рамках HTTP-запроса — он вполне может порождать внутренние операции, каждая из которых живет в отдельной транзакции, или какие-то в транзакции, а какие-то нет, ну и так далее. Впрочем…


    Это [решение, которое позволяет иметь единый контейнер зависимостей (IoC контейнер) на протяжении всего жизненного цикла запроса] может понадобиться в том случае, если web-приложение должно иметь транзакционность

    … если вам нужна общая транзакция на весь запрос, просто заверните запрос в TransactionScope в первом middleware. Использовать для этого DI-контейнер совершенно не обязательно.


    Что, само по себе, не отменяет удобства DI-контейнера для построения приложений вообще и наличия в нем per-request-зависимостей в частности.

© Habrahabr.ru