الگوی CQRS به روشی که از ابتدا باید میبود 🚀
📢 MediatR
در حال تجاری شدن است.
جیمی بوگارد اعلام کرد که MediatR برای شرکتهای بالاتر از یک اندازه مشخص، یک مدل لایسنس تجاری اتخاذ خواهد کرد.
برای بسیاری از تیمها، این یک محرک برای ارزیابی مجدد استفاده از آن و احتمالاً جستجو برای جایگزینها است.
و زمان بدی هم برای این کار نیست. MediatR تقریباً با CQRS در NET. مترادف شده است، با وجود این واقعیت که CQRS و MediatR یک چیز نیستند. اکثر پروژهها از آن به عنوان یک لایه ارسال (dispatching) نازک برای کامندها و کوئریها استفاده میکنند - یک مورد استفاده که میتواند با چند انتزاع (abstraction) ساده پوشش داده شود.
با حذف MediatR، شما به دست میآورید:
✅ کنترل کامل بر زیرساخت CQRS خود
✅ ارسال قابل پیشبینی و صریح به handlerها
✅ دیباگ و آنبوردینگ سادهتر
✅ راهاندازی تمیزتر DI و تستپذیری بهتر
📌ما پوشش خواهیم داد:
🔹 تعریف قراردادهای ICommand, IQuery و handlerها
🔹 افزودن پشتیبانی برای دکوراتورها (لاگینگ، اعتبارسنجی و غیره)
🔹 ثبت همه چیز با DI
🔹 یک مثال کامل و کاربردی در یک سناریوی دنیای واقعی
کامندها، کوئریها و Handlerها 🧱
بیایید با تعریف قراردادهای پایه برای کامندها و کوئریها شروع کنیم.
// ICommand.cs
public interface ICommand;
public interface ICommand<TResponse>;
// IQuery.cs
public interface IQuery<TResponse>;
این اینترفیسها صرفاً به عنوان نشانگر وجود دارند. آنها به ما اجازه میدهند منطق اپلیکیشن را حول نیت ساختار دهیم - عملیات نوشتن از طریق ICommand، عملیات خواندن از طریق IQuery.
اینترفیسهای handler از همان مدل پیروی میکنند:
// ICommandHandler.cs
public interface ICommandHandler<in TCommand> where TCommand : ICommand
{
Task<Result> Handle(TCommand command, CancellationToken cancellationToken);
}
public interface ICommandHandler<in TCommand, TResponse> where TCommand : ICommand<TResponse>
{
Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken);
}
// IQueryHandler.cs
public interface IQueryHandler<in TQuery, TResponse> where TQuery : IQuery<TResponse>
{
Task<Result<TResponse>> Handle(TQuery query, CancellationToken cancellationToken);
}
اینها تقریباً با APIهای IRequest و IRequestHandler MediatR یکسان هستند، که مهاجرت را در صورت خروج از MediatR بسیار ساده میکند.
مثال عملی: Command Handler 👨💻
برای دیدن این انتزاعها در عمل، بیایید یک کامند را پیادهسازی کنیم که یک آیتم todo را به عنوان تکمیل شده علامتگذاری میکند.
// CompleteTodoCommand.cs
public sealed record CompleteTodoCommand(Guid TodoItemId) : ICommand;
// CompleteTodoCommandHandler.cs
internal sealed class CompleteTodoCommandHandler(...) : ICommandHandler<CompleteTodoCommand>
{
public async Task<Result> Handle(CompleteTodoCommand command, CancellationToken cancellationToken)
{
// ... (منطق بیزینس برای تکمیل کردن todo) ...
return Result.Success();
}
}
💡چند نکته مهم:
• کامند یک آبجکت مقدار تغییرناپذیر است (فقط داده، بدون رفتار).
• هندلر تمام منطق بیزینس را کپسوله میکند: اعتبارسنجی، تغییر وضعیت، ایجاد domain events و پایداری.
• هیچ mediator، ISender یا ارسال پنهانی وجود ندارد. handler مستقیماً از طریق انتزاعهای سفارشی ما فراخوانی میشود.
دکوراتورها 🎨
برای پشتیبانی از دغدغههای مشترک (cross-cutting concerns) مانند لاگینگ، اعتبارسنجی و تراکنشها، ما الگوی دکوراتور را در اطراف handlerهای خود اعمال میکنیم.
بیایید به دو مثال نگاه کنیم: یکی برای لاگینگ، یکی برای اعتبارسنجی.
دکوراتور لاگینگ: 📝
internal sealed clasa LoggingCommandHandler<TCommand, TResponse>(...)
: ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
public async Task<Result<TResponse>> Handle(TCommand command, CancellationToken cancellationToken)
{
// ... (منطق لاگ کردن قبل و بعد از اجرای handler اصلی) ...
}
}
این کلاس هر ICommandHandler را میپیچد و لاگینگ ساختاریافته را در اطراف اجرای کامند اضافه میکند.