Целью данной статьи является поиск рабочего решение, которое позволяет иметь единый контейнер зависимостей (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<IUnityContainer>(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<IUnityContainer>(
                HttpApplicationKey.OwinPerRequestUnityContainerKey);

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

        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<IUnityContainer>(
                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<IUnityContainer>(HttpApplicationKey.OwinPerRequestUnityContainerKey);

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

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

var container = request.GetOwinContext().Get<IUnityContainer>(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<IAlwaysTheSame, AlwaysTheSame>(new ContainerControlledLifetimeManager());
    container.RegisterType<IAlwaysTheSame, AlwaysTheSame>(new ContainerControlledLifetimeManager());

    // HierarchicalLifetimeManager - container's lifetime
    container.RegisterType<ISameInARequest, SameInARequest>(new HierarchicalLifetimeManager());

    // TransientLifetimeManager (RegisterType's default) - no lifetime
    container.RegisterType<IAlwaysDifferent, AlwaysDifferent>(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
Поделиться с друзьями
-->

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


  1. NLO
    28.09.2016 22:17

    НЛО прилетело и опубликовало эту надпись здесь


    1. fsou11
      28.09.2016 22:20

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


    1. lair
      28.09.2016 22:50

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

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


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

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


      1. NLO
        29.09.2016 01:05

        НЛО прилетело и опубликовало эту надпись здесь


        1. lair
          29.09.2016 01:13
          +1

          Какой-такой объект, требует per-request lifetime…

          Контекст запроса, например. Или кэши.


          И разве это не «глубоко запрятано» в таком случае?

          Я продолжаю не понимать, что именно у вас "глубоко запрятано". Можете привести конкретный пример?


          1. Biorobotz
            29.09.2016 09:01
            -2

            Попробуй через принт задебажить


          1. NLO
            29.09.2016 10:32

            НЛО прилетело и опубликовало эту надпись здесь


            1. mayorovp
              29.09.2016 11:19

              Чем this.GetCacheManager() лучше параметра cacheManager, переданного к конструктор?


              Или открытого свойства CacheManager (хоть это и антипаттерн)?


              В первом случае, если я вдруг захочу эту же логику выполнить не из реквеста, а из консольного приложения. Я буду видеть, что мне каким-то образом надо отвязаться от GetCacheManager (скорее всего просто не использовать его вообще). А вот с использованием IoC контейнера данный факт будет не совсем очевидным.

              А с использованием IoC надо будет убрать из конструктора параметр cacheManager и добавить другой, который вам действительно нужен.


              Или, если cacheManager закопан в инфраструктуре — сделать NullCacheManager. Но его в таком случае придется делать в любом случае, хоть с IoC, хоть без.


              1. NLO
                29.09.2016 12:31

                НЛО прилетело и опубликовало эту надпись здесь


                1. mayorovp
                  29.09.2016 12:41
                  +1

                  Конструктор не является частью внешнего интерфейса.


                  1. NLO
                    29.09.2016 12:57

                    НЛО прилетело и опубликовало эту надпись здесь


                    1. lair
                      29.09.2016 13:01

                      … вот только зависимости обычно строятся не от класса, а от интерфейса. В котором конструкторов нет (ну, в .net, по крайней мере).


                      1. NLO
                        29.09.2016 13:11

                        НЛО прилетело и опубликовало эту надпись здесь


                        1. mayorovp
                          29.09.2016 13:13
                          +1

                          Вся суть DI в том, чтобы убрать это сцепление.


                          1. NLO
                            29.09.2016 13:21

                            НЛО прилетело и опубликовало эту надпись здесь


                        1. lair
                          29.09.2016 13:17

                          Если вы используете dependency inversion, то клиентский код никогда не вызывает конструктор. Поэтому нет, не добавляет.


                          1. NLO
                            29.09.2016 13:38

                            НЛО прилетело и опубликовало эту надпись здесь


                            1. lair
                              29.09.2016 14:01
                              +2

                              они оба завязаны на контейнер… это прям по Фаулеру

                              Нет. Контейнер не обязателен для DI.


                              сделать такое C который будет зависеть от них обоих…

                              Вот именно что C зависит от A и B, а вот они от C не зависят. В этом и пойнт.


                    1. mayorovp
                      29.09.2016 13:11

                      Это настолько древняя традиция, что даже не знаю где искать пруфы.


                      Могу привести вот какое соображение. Приглядитесь — в языке есть интерфейсы — но у интерфейса нельзя определить конструктор. Конструктор не может быть виртуальным. Вызывая конструктор класса — мы всегда вызываем конкретную реализацию.


                      Я знаю только 1 язык, в котором конструктор можно было включить во внешний интерфейс класса. Это был Object Pascal (=Delphi).


                      1. NLO
                        29.09.2016 13:23

                        НЛО прилетело и опубликовало эту надпись здесь


                        1. mayorovp
                          29.09.2016 13:24

                          В javascript инкапсуляции вовсе нет — но внешние интерфейсы есть.


                          1. NLO
                            29.09.2016 13:35

                            НЛО прилетело и опубликовало эту надпись здесь


                            1. mayorovp
                              29.09.2016 13:41

                              Приватные члены через замыкания давно уже признаны антипаттерном. Потому что ломают наследование.


            1. lair
              29.09.2016 11:43
              +1

              Так как я выше писал контекст запроса и есть внутренние потроха фреймворка.

              Совершенно не обязательно. Текущий пользователь? Роли текущего пользователя? Права и привилегии текущего пользователя? Это все весьма независимо от фреймворка (сугубо бизнесовых примеров тоже есть, но для них надо предметную область объяснять).


              Per-request кеш на мой взгляд это не что иное как например Bag (или другая коллекция на уровне реквеста). Так зачем тогда придумывать велосипед…

              Чтобы отвязаться от конкретной реализации.


              Я могу выполнять вот такой extension метод на контроллере this.GetCacheManager()

              Можете. И теперь вам надо протащить контроллер везде, где вам нужен этот кэш — в том числе, в прикладную область, если вдруг в ней есть кэши.


              Давайте на примере.


              class SchemaController: ApiController
              {
                private readonly IMetadataProvider _metadataProvider;
              
                public SchemaController(IMetadataProvider metadataProvider)
                {
                  _metadataProvider = metadataProvider;
                }
              
                public Schema Get(string version)
                {
                  return _metadataProvider.Get(version);
                }
              }

              Все прекрасно, все работает. Теперь мне надо внутрь конкретной реализации IMetadataProvider добавить кэширование (и, чтобы не возиться с инвалидацией, сделать кэширование продолжительностью в запрос пользователя, потому что на этом промежутке данные считаются консистентными). Чтобы использовать ваш подход, мне надо в Get передать либо сам CacheManager (наружу просочились детали реализации), либо this (и тогде кроме утекания реализации я еще получу зависимость провайдера от WebAPI). Если же я просто останусь в рамках DI, я могу либо вкинуть в конструктор провайдера нужный мне ICacheManager, либо вообще кэшировать данные в собственных полях реализации, а провайдер просто привязать к запросу. И, что характерно, оба этих варианта совершенно прозрачны для приведенного мной кода — он даже не знает, что мы сделали оптимизацию. Если потом мы передумаем и уберем оптимизацию, или, наоборот, сделаем кэш более долгоживущим — код-пользователь останется неизменным.


              Если же я отдаю управление жизненным циклом IoC контейнеру, то это уходит за пределы программной инфраструктуры веб приложения.

              И это прекрасно, потому что теперь мы можем выполняться не только в контексте веб-приложения.


              В первом случае, если я вдруг захочу эту же логику выполнить не из реквеста, а из консольного приложения. Я буду видеть, что мне каким-то образом надо отвязаться от GetCacheManager (скорее всего просто не использовать его вообще).

              А в случае с DI вы просто сделаете другую реализацию для ICacheManager, от которого зависит реализация IMetadataProvider, или же — еще проще — поменяете длину жизненного цикла самого провайдера.


              А вот с использованием IoC контейнера данный факт будет не совсем очевидным.

              Ну так пользователю (коду) и не надо знать, что там внутри кэширование. А для реализации все зависимости очевидны.


              1. NLO
                29.09.2016 12:50

                НЛО прилетело и опубликовало эту надпись здесь


                1. lair
                  29.09.2016 12:59

                  Мне вообще не нужен экземпляр класса User для выполнения логики, если это не апдейт свойств самого User.

                  Ну да, имя-фамилия для приветствия из воздуха берутся, как и url аватарки.


                  Так что идентификация пользователя для входящих запросов это целиком часть инфраструктуры веб приложения.

                  Идентификация — да. Но вот результат этой идентификации — уже нет.


                  И если вызываемая в методе контроллера логика завязана на айди юзера…

                  Она завязана не на id, а на свойства, которые в разных местах нужны разные.


                  И роли я не кеширую в куках… А проверяю всегда в момент выполнения бизнес логики.

                  А вот это уже как раз детали реализации User (точнее, если уж иметь в виду .net — IPrincipal): он может брать их оттуда, откуда сочтет нужным.


                  Вы не отвязываетесь от реализации:

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


                  1. NLO
                    29.09.2016 13:17

                    НЛО прилетело и опубликовало эту надпись здесь


                    1. lair
                      29.09.2016 13:19

                      если вам надо это на профиле или во время логина пусть эти данные возращаются соотвествующей логикой…

                      если вам они надо при отображении некоторых страниц — пусть вернет соответвующая логика -> положите в Bag и выцепите из Razor View

                      Вот оно и размазалось. А зачем мне делать по-разному, когда я могу сделать униформно?


                      зачем мне дважды выбирать пользователя из базы?

                      Вот и я про то, что незачем — намного лучше один раз достать, и потом под ним все делать.


                      а ничего что он будет за пределами основной транзакции?

                      Скорее всего — ничего.


                      1. NLO
                        29.09.2016 13:31

                        НЛО прилетело и опубликовало эту надпись здесь


                        1. lair
                          29.09.2016 13:59
                          +1

                          ничего там не размазывается… выбрать пользователя — это часть transaction script'a

                          Только если вы решите так сделать. И тогда у вас это будет в каждом скрипте, который от этого зависит.


                          а это вообще недопустимо… если на момент совершения транзакции пользователь заблокирован и не может осцществлять такую операцию это по вашему нормально да?

                          Это зависит от бизнес-требований. Системы, в которых важен отзыв полномочий за единицы миллисекунд, конечно, встречаются, но для них просто применяются немного другие решения. Заметим, именно "немного" — просто User, который передается между логическими слоями, не имеет собственных данных, а каждый раз проверяет их против БД. Но в реальности это настолько неотличимо от ситуации "права отобрали на миллисекунду после транзакции", что можно не заморачиваться.


                          операция проверки и собственно action должны быть единой атомарной операцией в таком случае…

                          Это, конечно, очень круто и правильно, но медленно.


        1. mayorovp
          29.09.2016 09:05

          При использовании ORM очень удобно создавать контекст для доступа к БД один на запрос. Это позволит передавать непосредственно хранимые сущности между компонентами и максимально использовать ленивую загрузку.


          Да, такой код потом придется переписывать, но пока у проекта всего один пользователь одновременно, веб-сервер на одном хосте с сервером СУБД и каждый день меняются требования — такой подход может сэкономить больше времени чем уйдет на переписывание позже.


          1. NLO
            29.09.2016 10:55

            НЛО прилетело и опубликовало эту надпись здесь


            1. mayorovp
              29.09.2016 11:15

              Вы даже не будете точно знать в какой именно момент выполняемого transaction script вы будете брать те или иные данные, что в некоторых случая может приводить к блокировкам, притом иногда к deadlock.

              Говорю же — не больше 1 пользователя онлайн (либо разработчик, либо заказчик). В исключительных ситуациях — целых два :)


              Но вы здесь говорите о меняющихся требованиях, что наводит на мысль, что вы все-таки хотите это все поддерживать.

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


            1. lair
              29.09.2016 11:44

              Вы не думали, что сам факт использования ORM, должен быть не доступен вашему Web приложению?

              Это почему вдруг?


              1. NLO
                29.09.2016 11:54

                НЛО прилетело и опубликовало эту надпись здесь


                1. lair
                  29.09.2016 11:56

                  Совершенно.


                  1. NLO
                    29.09.2016 12:23

                    НЛО прилетело и опубликовало эту надпись здесь


                    1. lair
                      29.09.2016 12:28
                      +1

                      А если у меня 2-tier (веб-приложение и СУБД)?


                      Ну и да, в случае добавления дополнительных уровней, у вас на первом уровне ORM-контекст заменяется на контекст соединения со следующем уровнем, но фраза — про удобство использование одного такого контекста на запрос — в общем остается верной.


                      1. NLO
                        29.09.2016 13:10

                        НЛО прилетело и опубликовало эту надпись здесь


                        1. mayorovp
                          29.09.2016 13:16
                          +1

                          Ага, конечно. Надо любое приложение на 5 однопальцевых пользователей делать как минимум на ферме серверов! Вдруг завтра еще пара миллионов сотрудников прибудет?


                        1. lair
                          29.09.2016 13:17

                          пару минусов сверху и этот ваш ответ только подтверждает, что большинство девелоперов и не заморачивается многослойностью… и лепят все на коленке.
                          а потом когда надо сделать ферму апи серверов или интеграцию или шарить код на несколько платформ — не знают что делать…

                          Вы так говорите, как будто нельзя ферму/апи/интеграцию на 2-tier сделать.


                          Ну и да, заморачиваться раньше времени — плохое дело, можно не дождаться того момента, когда заморочки оправдают себя.


                          я предположил что раз вам нужен middleware

                          А где я говорил, что мне нужен middleware?


                          у вас же там нету даже двух слоев…

                          Уровней. Есть: веб и СУБД, физически разделены (я, так и быть, не буду тонкий клиент считать).


                          измение СУБД (в которой например будут не совместимые типы данных, или noSQL котрому не нужен ваш ORM)

                          Хороший ORM для того и нужен, чтобы компенсировать несовместимые типы данных. А если у меня переход с реляционки на noSQL, то все равно изменений будет столько, что одной абстракцией не обойдешься.


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


                          и поскольку мы сейчас не говорим об n-tier

                          Да не меняется ничего в n-tier, только вместо контекста ORM у вас контекст соединения к следующему слою (собственно, ORM-контекст им и является, так что даже не "вместо", а просто обобщение).


                          1. NLO
                            29.09.2016 13:43

                            НЛО прилетело и опубликовало эту надпись здесь


                            1. lair
                              29.09.2016 14:03
                              +1

                              без слеша… использующую код вашего 2-tier приложения? нет нельзя…

                              Почему? У меня вон под боком работает.


                              мне такой абтракции мало

                              Если конкретно вам в вашем случае мало, то это еще не значит, что всем остальным не хватит.


  1. lair
    28.09.2016 22:58
    +3

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

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


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

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


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

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


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


    1. mayorovp
      29.09.2016 09:28
      +1

      Внутри TransactionScope транзакции имеют тенденцию становиться распределенными, а распределенных транзакций на пустом месте надо избегать.


      1. NLO
        29.09.2016 11:10

        НЛО прилетело и опубликовало эту надпись здесь


        1. mayorovp
          29.09.2016 11:26

          У меня была ситуация, когда EF во время сохранения изменений два раза подключалась к активному скоупу — из-за чего транзакция становилась распределенной. Но сейчас воспроизвести не смог — наверное, за несколько лет баг починили.


          А транзакционность операция над двумя базами я привык обеспечивать идемпотентными операциями. Почему-то сложных случаев, когда это невозможно, мне не попадалось.


          1. NLO
            29.09.2016 12:20

            НЛО прилетело и опубликовало эту надпись здесь


            1. lair
              29.09.2016 12:30
              +1

              что вам даст идемпотентность? ничего…

              Идемпотентность дает возможность многократно накатывать операцию до тех пор, пока она наконец не пройдет. Это позволяет обойтись без распределенной транзакции.


            1. mayorovp
              29.09.2016 12:38
              +1

              Если из системы А в Б ушло 500 попугаев — то:


              1. создается запись в А об ушедших попугаях, транзакция завершается;
              2. создается запись в Б о пришедших попугаях, транзакция завершается;
              3. создается запись в А о успешном создании записи в Б, транзакция завершается.

              Шаги 2-3 повторяются в фоне до тех пор пока в системе А есть запись о передаче попугаев в Б, но нет отметки об успехе.


              Принципиальное отличие от распределенной транзакции — в том, что ни одна из записей не блокируется на неопределенное время.


      1. lair
        29.09.2016 11:46

        На моей памяти TransactionScope не замечен за эскалацией транзакций на одном ресурсе. А если ресурсов больше, то все равно распределенная понадобится.


        Я при этом не говорю, что оборачивать весь запрос в (распределенную или нет) транзакцию — это хорошо (я скорее считаю, что это плохо). Я просто говорю, что если это вдруг надо, не обязательно per-request-зависимости использовать.


        1. mayorovp
          29.09.2016 12:40

          Там, как я понял, было два ресурса. База данных и кэш контекста в памяти клиента. Подробностей не помню.


    1. fsou11
      29.09.2016 12:42

      Я посмотрел использование Autofac с Web API и OWIN'ом,- используется аналогичный моему решению принцип, в котором сконфигурированный контейнер (scope) привязывается к контексту Owin'a, а затем с помощью нового MessageHandler'a достаётся из контекста и перегружает стандартный scope Web API.

      Однако, мне осталось не ясен момент, как же резолвятся сами контроллеры, т.к. стандартный ControllerActivator на полученном scope по прежнему вызывает BeginScope (default'ная реализация поведения создания контроллеров в web api).


      1. lair
        29.09.2016 12:54
        +1

        Я посмотрел использование Autofac с Web API и OWIN'ом,- используется аналогичный моему решению принцип

        Ну да, только пользовательского кода намного меньше.


        Однако, мне осталось не ясен момент, как же резолвятся сами контроллеры, т.к. стандартный ControllerActivator на полученном scope по прежнему вызывает BeginScope

        Не вызывает, на самом деле. Вызывает он GetDependencyScope, который вопреки тому, что у вас написано, вызывает вызывает BeginScope только в том случае, если скоупа нет в контексте.


        1. fsou11
          29.09.2016 13:08

          Понял, значит вместо создания собственного активатора достаточно было в первой Middleware переопределить скоуп, который находится в свойстве request.Properties.[HttpPropertyKeys.DependencyScope].

          Проверил autofac,- всё действительно так, как вы говорите (сверху controller, снизу middleware):

          image


          1. lair
            29.09.2016 13:10

            Ну вот да, все же просто на самом деле.


            1. fsou11
              01.10.2016 12:23

              Однако, нет. Непосредственно сам объект HttpRequestMessage появляется уже после захода в Web API,- на этапе конфигурации OWIN Middleware'ов он не доступен, как следствие: либо делать через MessageHandler (Autofac) либо так, как выше.


              1. lair
                01.10.2016 12:48

                Что сложного в MessageHandler? Это фундаментальная единица расширения WebAPI.


                Не говоря уже о том, что для пользователя Autofac даже и это сокрыто — он имеет дело с вызовом одного метода.


                1. fsou11
                  01.10.2016 16:14

                  Я не говорю что это сложно, но отмечаю, что без переопределения/расширения самого Web API вклинится в контейнер (например, извне) не получится.


  1. fsou11
    29.09.2016 13:07

    Мимо, ап.


  1. dotnetdonik
    30.09.2016 09:12

    До тех пор пока Web Api 2 хоститься под IIS по идее в Unity должен нормально работать PerHttpRequestModule и не важно используете вы OwinMiddleware в своем приложении или нет. Описанный выше кейс более актуальный для self host. + судьба Unity достаточно туманна в текущий момент, год назад его мс передали каким-то ребятам его, и до сих пор не было аннонсированно ни обновлений ни будущих планов на эту либу.


    1. fsou11
      30.09.2016 11:08

      Поиск по репозиторию Unity

      PerHttpRequestModule
      не выдал результатов.
      PerRequestLifetimeManager
      является частью Microsoft.Unity.Mvc, но не Web API.


      1. dotnetdonik
        30.09.2016 12:31

        PerRequestLifetimeManager работает под Web Api и под MVC одинаково, когда вы используете IIS для хостинга так же и как и все остальные HttpModules в IntegratedPipeline.
        Вы не первый кому понадобилось использовать Shared DbContext в Web Api и не первый, кто словил исключение при попытке сделать join при получении данных.


        1. fsou11
          30.09.2016 12:56

          Он ведь основан на HttpContext, который при OWIN host'e отсутствует?


  1. dotnetdonik
    30.09.2016 13:56

    OWIN это просто абстракция, а не хост. Любой веб сервер может быть OWIN совместимым, как и IIS. System.Web.HttpContext же отсутствует только если вы не используете IIS.

    https://msdn.microsoft.com/en-us/library/dn270723(v=vs.113).aspx