Тэкс, у меня есть пример попытки выстроить кое-что поверх CQRS. Но и сам CQRS он частично иллюстрирует. Увы, это не рабочий проект, поэтому там много нет.
Вот файл с набором сообщений и их обработчиков:
Например, глянем на пару EmailExists и EmailExistsHandler.
—
EmailExists является формулировкой запроса. Мы говорим, что хотим проверить, существует ли такой email в бд. Там в самом классе еще всякий мусор есть (ToString, Equals, GetHashCode), который к самому CQRS не относится. Сводится оно к этому:
class EmailExists : IQuery<bool> {
public string Email { get; set; }
}
Тут минимум информации: что нужно узнать (имя класса EmailExists), с какими параметрами (свойство Email), и что вернется в результате (bool).
—
Дальше, у нас есть EmailExistsHandler. Это обработчик запроса. Поскольку это семпл, то тут нету работы с бд. Но оно должно быть в рабочем проекте, так что представим, что мы добавляем эту логику. Тогда нужно ожидать в конструкторе какой-нибудь DbConnection. И в методе Handle мы будем дергать бд и возвращать данные.
class EmailExistsHandler : IQueryHandler<EmailExists, bool> {
private DbConnection dbConnection; EmailExistsHandler(DbConnection dbConnection) {
this.dbConnection = dbConnection;
} public async Task<bool> Handle(EmailExists query) {
// do something with dbConnection and query
}
}
Тут видно, что нам, увы, приходится дублировать тип “bool”. Я пока не знаю как от этого избавиться.
—
Связь между сообщением и его обработчиком мы будем описывать через DI-контейнер. Совсем примитивные нам не подойдут, они не умеют в generics. С нормальными контейнерами можно сделать так:
diContainer.Register<
IQueryHandler<EmailExists, bool>,
EmailExistsHandler
>()
И это будет работать.
Опять же, этот паттерн регистрации автоматизируемый. Так что если у вас много типов сообщений, то лучше автоматизировать регистрацию.
—
Теперь посмотрим на сам вызов:
Вызов выглядит вот так:
var userExists = await bundle.Query(new EmailExists() { Email = email });
Нам не нужно знать, какие зависимости нужны EmailExistsHandler. Это разруливает DI. В этом преимущество разделения на сообщение и обработчик. С другой стороны, если вы не используете DI, то вам это разделение не особо нужно.
С третьей стороны, DI позволяет писать универсальную кэширующую прослойку на все обработчики сообщений.
—
Сам пример не иллюстрирует структуру проекта, потому что это PoC. Структура проекта — сложный вопрос. В принципе, для начала вы спокойно можете себе оставаться в парадигме DAL/BAL разделения. Просто у вас будут DAL Query и BAL Query.
Дальнейшее продвижение заключается в том, чтобы делить Query на следующие: публичные и приватные.
Публичные могут использоваться кем угодно. Приватные могут использоваться с ограничениями (обычно внутри модуля).
Примеров без NDA на это, увы, нет.