CQRS расширенный курс

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

В центре

Dispatcher – это ключевой элемент в работе CQRS, который выполняет Command и Query в рамках одной транзакции, реализуя паттерн Unit Of Work, а также позволяет воспользоваться всеми преимуществами AOP без дополнительных плагинов или сторонних библиотек. Рассмотрим использование Unit Of Work и AOP в Dispatcher.

Unit Of Work: в CommandBase основной код содержится в перегруженном методе Execute, для соблюдения паттерна этот метод вызывается Dispatcher:

Однако Execute можно вызывать без участия Dispatcher – в этом случае будет открыто подключение к базе данных и создана транзакция.

 примечание: если реализовывать Unit Of Work в каждой Command или Query, то будет очень высокий дубляж кода в методах Execute.

AOP: аспектно ориентированное программирование в C# реализовано по большей части в рамках Reflection и Attributes – для этого приходится дополнительно отмечать методы атрибутами, а также реализовывать систему чтения атрибутов в процессе выполнения кода. Есть инструменты, такие как PostSharp, которые позволяют реализовать это на более высоком уровне, но они являются платными, а также требуют дополнительных абстракций. Все Message, которые выполняются через Dispatcher, на определенных этапах выполнения делают publish событий, которые сигнализируют о том или ином состоянии.

  • OnBeforeExecuteEvent – событие наступает до выполнение Message. Cценарии применения: аудит, профилирования, проверка ограничений.
  • OnAfterExecuteEvent – событие наступает после удачного выполнения Message. Cценарии применения: аудит, профилирования, оповещения.
  • OnAfterErrorExecuteEvent – событие наступает, если при выполнении Message был выброшен Exception. Сценарии применения: аудит, обработка ошибок, оповещение.
  • OnCompleteExecuteEvent – событие наступает после выполнения Message даже если ошибка была, то есть данное событие группирует поведение After Execute и After Error Execute. Cценарии применения такие же, как и для OnAfterExecuteEvent и OnCompleteExecuteEvent.

Пример

Все события имеют доступ к текущей выполняемой Message, что позволяет реализовать даже самые сложные сценарии, рассмотрим код проверки на наличие прав доступа:

  • строка 12 – получив доступ к текущему выполняемому  Message, пробуем привести его к интерфейсу IRoleMessage.
  • строка 13 – если roleMessage null, то ограничений нет.
  • строка 16 – получаем User на основе имеющегося UserId.
  • строка 17 – получаем через рефлексию instance атрибута Role.
  • строка 18 – проверяем права.
  • строка 19 – “выбрасываем” exception если проверка прав не прошла.

 

Дополнительная декомпозиция

CQRS помогает разделять ответственность между классами, что покрывает множество задач. Но бывают сценарии, которые даже после разделения на Command и Query трудно поддерживать и тестировать из-за большого объема бизнес-логики и условий. Для решения этой проблемы в Incoding Framework существует два способа:

1.Composite – позволяет выполнять N-ое количество Command или Query в рамках одной транзакции, что дает возможность разделить сложный сценарии на ещё более мелкие части.

Рассмотрим данный способ на примере.

  Условие: Пошаговая регистрация объявления:

  • Шаг 1 – Заполнение полей.
  • Шаг 2 – Подключение дополнительных опций.
  • Шаг 3 – Оплата.

Ограничение: Все три шага должны быть выполнены в рамках одной транзакции.

Проблема: Если весь код необходимый для выполнения сценария написать в одной Command, то получится сложное решение, которое будет трудно поддерживать.

примечание: метод Push имеет набор перегрузок, которые позволяют выполнять один Command или пакет.

  • строка 5 – ссылаемся на step 1.
  • строка 6 – ссылаемся на step 2.
  • строка 7 – ссылаемся на step 3.
  • строка 8 – после заполнения Composite идет выполнение по очереди каждой Command.

Листинг кода удовлетворяет всем требованиям задачи и все Commands будут выполнены в рамках одной транзакции, что гарантирует откат всех действий, если step 2 или step 3 “выбросит” exception.

Кроме группировки Command в один пакет Composite позволяет манипулировать результатами выполнения. Усложним задачу и поставим условие, чтобы Step1 после выполнения передавал Id нового элемента в Step 2 и 3.

  •  строка 5 – после удачного завершения Step1, будет выполнен action OnAfter, который проставит Result из Step1 в поле Id для Step 2 и 3.

2.Event Broker – брокер событий, используется в первую очередь для агрегации однотипных действий из разных Command и Query, также может быть применен для разделения ответственности в Message.

Рассмотрим данный способ на примере

  Условие: Отправка email после удачной регистрации пользователя.

  Проблема: дело в том, что основной целью Command в данном контексте является сохранение нового пользователя, а остальные действия рассматриваются как “после условия”. “После условия” могут быть сложными в плане реализации и будут добавлять “шумы” к основной реализации (например: отправка email по шаблону или работа с IMAP), что в итоге приведет к усложнению кода и проблемам поддержки и тестирования.

Листинг представляет код Command, где в последней строке делается публикация события, которое отвечает за отправку email – это позволяет разделить ответственность.

Кто же лучше ?

Event Broker

+ доступны переменные, которые “живут” в рамках метода Execute.
+ можно выполнять в не контекста Message.
+ повторно используется в разных Message.
– сам по себе не поддерживает транзакцию.

Composite

+ решает сложные атомарные задачи.
+ можно использовать повторно в не контекста Composite ( пример “Оплата за объявление”, которое можно вызывать не только в процессе регистрации, но и в других ситуациях ).
 – нельзя использовать в рамках других Message.

Вывод: Composite и Event Broker решают, на первый взгляд, одинаковые задачи, но у них есть различия, на основе которых можно проанализировав поставленную задачу и, определив в каком контексте требуется выполнение, можно легко выбрать подходящий способ для Вашего сценария.

А как быть если ..

Многие разработчики сталкивались с проблемой, когда для реализации проекта требуется помимо работы с основной базой ещё дополнительно подключиться к уже существующей базе заказчика, или когда к системе выдвигаются требования, чтобы была возможность смены connection string в runtime.

  1. Унаследованная система – чаще всего такая ситуация бывает, когда старый проект, написанный на WinForm, не имеет API. В данном случае лучшим решением будет написать свою обертку поверх “старой” базы. В рамках Incoding Framework имеются средства, которые позволяют избежать создания дополнительной инфраструктуры для работы с разными конфигурациями и разрабатывать Command и Query, не учитывая этой детали.

Реализация

Листинг кода демонстрирует регистрацию двух разных конфигураций баз данных, которые имеют разные схемы, и могут также иметь разных провайдеров (mssql,mysql, oracle).

Для того чтобы выполнить Query или Command в рамках определенной конфигурации, надо указать имя Instance в параметрах Dispatcher.Push или Dispatcher.Query:

  1. “Горячая” смена connection string – если приложение получает строку подключения не при старте, а в процессе работы, например, после входа в систему сторонний сервис выдает адрес на текущую сессию, то надо иметь возможность изменять путь указанный ранее.

Вывод: реализация CQRS в рамках Incoding Framework учитывает множество сценариев, с которыми сталкивается .net разработчик, и имеет набор средств для решения задач и улучшения (рефактор) кода.

Vlad Kopachinsky

I am a Senior developer at Incoding Software in Russian Federation. I created the Incoding Framework for rapid development

More Posts - Website - Twitter - Facebook - LinkedIn

Leave a Reply