Так уж случилось, что я достаточно часто начинаю новые проекты, где ещё нет ни структуры проектов, ни архитектуры. Делаю git init и dotnet new. А что дальше?
Есть много "привычных" архитектур, когда мы тащим из старого проекта утилитарные классы для БД, контейнера, конфигурации. А сам код организуем по аналогии с прошлым опытом (исправив, конечно, все ошибки прошлого и добавив новых во имя будущих нас).
Чтобы не замыкаться бесконечно в одном паттерне построения приложения, хочу посмотреть, как будет выглядеть CQRS с учётом текущего развития ASP.NET. Без всяких популярных MediatR, EventFlow и прочего. Только то, что есть внутри ASP.NET.
Туториал довольно базового уровня — посмотрим, как быстро делить логику приложения на изолированные команды и запросы, регистрировать это всё в DI, вытаскивать оттуда и научим это всё общаться через события. Репозиторий туть. Заодно чуть-чуть поработаем с Mininal APIs.
Команды и запросы
Для начала создадим пустой ASP.NET проект:
Мы хотим изолировать логику от уровня роутинга, валидации парамтров и прочго "C" в аббревиатуре MVC. CQS для этого предлагает раздлить всю работу с данными на 2 типа — запросы (которые только читают, но н изменяют данные) и команды (которые только делают действие, но ничего не возвращают). Нам понадобятся 2 интерфейса — для обработчика комманд и запросов:
Иногда в CQRS хочется чтобы комманда умела что-то возвращать (например, идентификатор созданной сущности), но в примере ограничимся "классическим" паттерном, когда команды ничего не возвращают.
Затем сделаем 2 метода-расширения, которые помогут нам добавлять обработчики, реализующие эти интерфейсы, в DI-контейнер.
Попробуем добавить какой-нибудь простой пример и посмотреть, как это работает. Сделаем сущность Post для статьи c заголовком и текстом, обработчик команды добавления нового поста и запроса извлечения списка постов.
Регистрация обработчиков в Program.cs будет выглядеть в этом случае примерно так:
Добавим магии Minimal APIs и ASP.NET Core чтобы посмотреть, как работает этот подход и магия DI в методах-обработчиках. Создадим 2 обработчика запросов:
Запускаем и проверяем, что всё работает.
Функциональность
Но ведь можно сделать лучше функциональней! Зачем таскать интерфейс, когда можно использовать делегаты и внедрять напрямую метод Handle.
Добавляем делегаты для запросов и команд, меняем регистрацию в контейнере. В итоге сама регистрация не изменится, а методы-расширения для DI будут выглядеть примерно так:
В методах-обработчиках внедренные из DI-контейнера интерфейсы изменятся на добавленные делегаты и будут вызываться напрямую:
Отлично, для базовой рализации этого достаточно.
События
Или нет?
Ещё один полезный инструмент, который может пригодиться — это генерация событий и выполнение некоторых действий (команд) по этому событию. Скажем, при создании поста нам хочется отправить его в сервис "Главреда" и проверять в посте орфографию.
И при этом добавить слой абстракции чтобы одни команды не знали о других, а просто генерировали события.
Интерфейс:
Метод-расширение для регистрации:
Пример генерации события из команды создания поста:
Обработчик события:
Сопоставление типов запроса и ответа
В Mediatr есть интерфейс IRequest<TResponse>, которые должны реализовывать все типы запросов. Этот интрфейс сам по себе не включает логики, но за счёт generic-парамтра можно сделать дополнитльное ограничние для IQueyHandler и соответствующего делегата, которое поможет найти ошибку при рефакторинге — если в запросе поменяется тип возвращаемого значения, то компилятор найдёт несоответствие типа парамтра в обработчике, запросе и инжектированном интрфейсе.
Такой подход уменьшает число ошибок в рантайме, но вынуждает программиста реализовать для типов запросов нефункциональный интерфейс.
Базовая реализация
Осталось вынести базовую обвязку для команд, запросов и событий в отдельную библиотеку, при желании добавить методов регистрации всех классов в сборке чтобы не бойлерплейтить код для DI, дать командам возможность возвращать значения, сделать отдельный тип для асинхронных задач, базовые реализации обработчиков с датаконтекстом или проверкой авторизации. Но это уже совсем project-specific история, а пока есть 60 строк кода, которые позволяют работать с командами, запросами и событиями в ASP.NET окружении.
Репозиторий чтобы посмотреть и потрогать.