Pull to refresh

Comments 65

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

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

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


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

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

UFO just landed and posted this here
Какой-такой объект, требует per-request lifetime…

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


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

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

Попробуй через принт задебажить
UFO just landed and posted this here

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


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


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

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


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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

UFO just landed and posted this here
они оба завязаны на контейнер… это прям по Фаулеру

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


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

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

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


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


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

UFO just landed and posted this here

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

UFO just landed and posted this here

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

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

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


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 контейнера данный факт будет не совсем очевидным.

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

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

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


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

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


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

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


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

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


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

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

UFO just landed and posted this here
если вам надо это на профиле или во время логина пусть эти данные возращаются соотвествующей логикой…

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

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


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

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


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

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

UFO just landed and posted this here
ничего там не размазывается… выбрать пользователя — это часть transaction script'a

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


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

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


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

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

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


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

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

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


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

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

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

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

UFO just landed and posted this here
UFO just landed and posted this here

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


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

UFO just landed and posted this here

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

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

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


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


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

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


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

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


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

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


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


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

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

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

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


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

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

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

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


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

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


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

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


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

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

UFO just landed and posted this here

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


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

UFO just landed and posted this here
что вам даст идемпотентность? ничего…

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

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


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

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


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

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


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

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

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

Однако, мне осталось не ясен момент, как же резолвятся сами контроллеры, т.к. стандартный ControllerActivator на полученном scope по прежнему вызывает BeginScope (default'ная реализация поведения создания контроллеров в web api).
Я посмотрел использование Autofac с Web API и OWIN'ом,- используется аналогичный моему решению принцип

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


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

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

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

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

image

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

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

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


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

Я не говорю что это сложно, но отмечаю, что без переопределения/расширения самого Web API вклинится в контейнер (например, извне) не получится.
До тех пор пока Web Api 2 хоститься под IIS по идее в Unity должен нормально работать PerHttpRequestModule и не важно используете вы OwinMiddleware в своем приложении или нет. Описанный выше кейс более актуальный для self host. + судьба Unity достаточно туманна в текущий момент, год назад его мс передали каким-то ребятам его, и до сих пор не было аннонсированно ни обновлений ни будущих планов на эту либу.
Поиск по репозиторию Unity
PerHttpRequestModule
не выдал результатов.
PerRequestLifetimeManager
является частью Microsoft.Unity.Mvc, но не Web API.
PerRequestLifetimeManager работает под Web Api и под MVC одинаково, когда вы используете IIS для хостинга так же и как и все остальные HttpModules в IntegratedPipeline.
Вы не первый кому понадобилось использовать Shared DbContext в Web Api и не первый, кто словил исключение при попытке сделать join при получении данных.
Он ведь основан на HttpContext, который при OWIN host'e отсутствует?
Sign up to leave a comment.

Articles

Change theme settings