Найти в Дзене

CQRS для ASP.NET без MediatR за 60 строк кода

Оглавление

Так уж случилось, что я достаточно часто начинаю новые проекты, где ещё нет ни структуры проектов, ни архитектуры. Делаю git init и dotnet new. А что дальше?

Есть много "привычных" архитектур, когда мы тащим из старого проекта утилитарные классы для БД, контейнера, конфигурации. А сам код организуем по аналогии с прошлым опытом (исправив, конечно, все ошибки прошлого и добавив новых во имя будущих нас).

Чтобы не замыкаться бесконечно в одном паттерне построения приложения, хочу посмотреть, как будет выглядеть CQRS с учётом текущего развития ASP.NET. Без всяких популярных MediatR, EventFlow и прочего. Только то, что есть внутри ASP.NET.

Туториал довольно базового уровня — посмотрим, как быстро делить логику приложения на изолированные команды и запросы, регистрировать это всё в DI, вытаскивать оттуда и научим это всё общаться через события. Репозиторий туть. Заодно чуть-чуть поработаем с Mininal APIs.

Команды и запросы

Для начала создадим пустой ASP.NET проект:

Команда консоли, которая создаст новый пустой asp.net проект
Команда консоли, которая создаст новый пустой asp.net проект
-2

Мы хотим изолировать логику от уровня роутинга, валидации парамтров и прочго "C" в аббревиатуре MVC. CQS для этого предлагает раздлить всю работу с данными на 2 типа — запросы (которые только читают, но н изменяют данные) и команды (которые только делают действие, но ничего не возвращают). Нам понадобятся 2 интерфейса — для обработчика комманд и запросов:

-3
Иногда в CQRS хочется чтобы комманда умела что-то возвращать (например, идентификатор созданной сущности), но в примере ограничимся "классическим" паттерном, когда команды ничего не возвращают.

Затем сделаем 2 метода-расширения, которые помогут нам добавлять обработчики, реализующие эти интерфейсы, в DI-контейнер.

-4

Попробуем добавить какой-нибудь простой пример и посмотреть, как это работает. Сделаем сущность Post для статьи c заголовком и текстом, обработчик команды добавления нового поста и запроса извлечения списка постов.

-5

Регистрация обработчиков в Program.cs будет выглядеть в этом случае примерно так:

-6

Добавим магии Minimal APIs и ASP.NET Core чтобы посмотреть, как работает этот подход и магия DI в методах-обработчиках. Создадим 2 обработчика запросов:

-7

Запускаем и проверяем, что всё работает.

Функциональность

Но ведь можно сделать лучше функциональней! Зачем таскать интерфейс, когда можно использовать делегаты и внедрять напрямую метод Handle.

Добавляем делегаты для запросов и команд, меняем регистрацию в контейнере. В итоге сама регистрация не изменится, а методы-расширения для DI будут выглядеть примерно так:

-8

В методах-обработчиках внедренные из DI-контейнера интерфейсы изменятся на добавленные делегаты и будут вызываться напрямую:

-9

Отлично, для базовой рализации этого достаточно.

События

Или нет?

Ещё один полезный инструмент, который может пригодиться — это генерация событий и выполнение некоторых действий (команд) по этому событию. Скажем, при создании поста нам хочется отправить его в сервис "Главреда" и проверять в посте орфографию.

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

Интерфейс:

-10

Метод-расширение для регистрации:

-11

Пример генерации события из команды создания поста:

-12

Обработчик события:

-13

Сопоставление типов запроса и ответа

В Mediatr есть интерфейс IRequest<TResponse>, которые должны реализовывать все типы запросов. Этот интрфейс сам по себе не включает логики, но за счёт generic-парамтра можно сделать дополнитльное ограничние для IQueyHandler и соответствующего делегата, которое поможет найти ошибку при рефакторинге — если в запросе поменяется тип возвращаемого значения, то компилятор найдёт несоответствие типа парамтра в обработчике, запросе и инжектированном интрфейсе.

-14

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

Базовая реализация

Осталось вынести базовую обвязку для команд, запросов и событий в отдельную библиотеку, при желании добавить методов регистрации всех классов в сборке чтобы не бойлерплейтить код для DI, дать командам возможность возвращать значения, сделать отдельный тип для асинхронных задач, базовые реализации обработчиков с датаконтекстом или проверкой авторизации. Но это уже совсем project-specific история, а пока есть 60 строк кода, которые позволяют работать с командами, запросами и событиями в ASP.NET окружении.

Репозиторий чтобы посмотреть и потрогать.

Nuget-пакет чтобы попробовать самому.

Telegram-канал Ростовского .net-сообщества.