اگه توی دنیای برنامهنویسی دات نت (.NET) فعال هستی، احتمالاً اسم CQRS به گوشت خورده. شاید اول نگرانت کرده باشه، ولی خوشبختانه این الگوی معماری به اندازهای که فکر میکنی پیچیده نیست.
CQRS مخفف Command Query Responsibility Segregation است. به زبان ساده یعنی جداسازی مسئولیت بین عملیات نوشتن (Command) و عملیات خواندن (Query) در اپلیکیشنات.
این مفهوم توسط Greg Young مطرح شد و از اصل قدیمیتری به نام CQS (Command Query Separation) که توسط Bertrand Meyer ابداع شده بود، الهام گرفته است. تفاوت اصلی اینه که CQRS این جداسازی رو در سطح معماری کل سیستم اعمال میکنه، نه فقط در سطح متدها.
توی روش سنتی، یه مدل واحد برای هر دو عملیات خواندن و نوشتن استفاده میکنیم. این روش که بهش CRUD میگن، توی پروژههای کوچیک خوبه، ولی وقتی پروژه بزرگ میشه، دردسرهای جدی شروع میشن:
یه Command یه درخواست برای تغییر وضعیت سیستم است. Command هیچ مقداری برنمیگردونه (یا فقط یه تأیید ساده برمیگردونه). مثال:
CreateOrderCommandUpdateProductCommandDeleteUserCommandیه Query یه درخواست برای خواندن اطلاعات است. Query هیچوقت وضعیت سیستم رو تغییر نمیده. مثال:
GetOrderByIdQueryGetAllProductsQueryGetUserProfileQueryبرای هر Command و هر Query یه Handler جداگانه داریم که مسئول پردازش اون درخواسته. این جداسازی باعث میشه هر Handler تنها یه مسئولیت داشته باشه (اصل Single Responsibility).
در دنیای دات نت، محبوبترین ابزار برای پیادهسازی CQRS کتابخانه MediatR است که توسط Jimmy Bogard ساخته شده. این کتابخانه یه واسطه (Mediator) بین درخواستها و پردازندههاشون ایجاد میکنه و کدت رو خیلی تمیز میکنه.
با استفاده از NuGet Package Manager یا dotnet CLI:
dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
بریم سراغ یه مثال واقعی. میخوایم یه سیستم ساده مدیریت سفارش بسازیم.
public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
Pending,
Confirmed,
Shipped,
Delivered
}
// Command public class CreateOrderCommand : IRequest{ public string CustomerName { get; set; } public decimal TotalAmount { get; set; } } // Command Handler public class CreateOrderCommandHandler : IRequestHandler { private readonly IOrderRepository _repository; public CreateOrderCommandHandler(IOrderRepository repository) { _repository = repository; } public async Task Handle( CreateOrderCommand request, CancellationToken cancellationToken) { var order = new Order { CustomerName = request.CustomerName, TotalAmount = request.TotalAmount, CreatedAt = DateTime.UtcNow, Status = OrderStatus.Pending }; await _repository.AddAsync(order, cancellationToken); return order.Id; } }
// Query DTO
public class OrderDto
{
public int Id { get; set; }
public string CustomerName { get; set; }
public decimal TotalAmount { get; set; }
public string Status { get; set; }
}
// Query
public class GetOrderByIdQuery : IRequest
{
public int OrderId { get; set; }
}
// Query Handler
public class GetOrderByIdQueryHandler
: IRequestHandler
{
private readonly IOrderRepository _repository;
public GetOrderByIdQueryHandler(IOrderRepository repository)
{
_repository = repository;
}
public async Task Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
var order = await _repository
.GetByIdAsync(request.OrderId, cancellationToken);
if (order == null) return null;
return new OrderDto
{
Id = order.Id,
CustomerName = order.CustomerName,
TotalAmount = order.TotalAmount,
Status = order.Status.ToString()
};
}
}
[ApiController]
[Route(“api/[controller]”)]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task CreateOrder(
[FromBody] CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return CreatedAtAction(
nameof(GetOrder),
new { id = orderId },
orderId);
}
[HttpGet(“{id}”)]
public async Task GetOrder(int id)
{
var result = await _mediator.Send(
new GetOrderByIdQuery { OrderId = id });
if (result == null) return NotFound();
return Ok(result);
}
}
یکی از قویترین ویژگیهای MediatR، Pipeline Behavior است. با این قابلیت میتونی Cross-cutting Concerns مثل Logging، Validation و Caching رو بدون آلوده کردن Handlerهات پیاده کنی.
public class ValidationBehavior: IPipelineBehavior where TRequest : IRequest { private readonly IEnumerable > _validators; public ValidationBehavior( IEnumerable > validators) { _validators = validators; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { if (!_validators.Any()) return await next(); var context = new ValidationContext (request); var validationResults = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, cancellationToken))); var failures = validationResults .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); if (failures.Count != 0) throw new ValidationException(failures); return await next(); } }
خیلی وقتها CQRS رو در کنار Event Sourcing میبینیم. در Event Sourcing به جای ذخیره وضعیت نهایی، تمام رویدادهایی که منجر به اون وضعیت شدن رو ذخیره میکنیم.
این ترکیب مزایای زیادی داره:
خیلی از کسبوکارها هر روز مشتری از دست میدن چون سایتشون دیده نمیشه. سئو یه سرمایهگذاری مطمئنه که بهجای هزینه تبلیغات مداوم، یه بار درست انجام بشه و سالها برات درآمد بیاره.
آیا میخواهید سایت شما هم مثل رقبا در صفحه اول گوگل باشد و زنگخورهایتان چند برابر شود؟
سئوی سایت خود را به متخصصان ما بسپارید. همین حالا برای مشاوره رایگان با ما تماس بگیرید:
📞 09190994063 - 09376846692
این سوال خیلی مهمه. CQRS یه Silver Bullet نیست و برای همه پروژهها مناسب نیست.
یه ساختار خوب و استاندارد برای پروژه CQRS در دات نت اینطوریه:
📁 src/ 📁 Application/ 📁 Orders/ 📁 Commands/ 📁 CreateOrder/ ├── CreateOrderCommand.cs ├── CreateOrderCommandHandler.cs └── CreateOrderCommandValidator.cs 📁 UpdateOrder/ ├── UpdateOrderCommand.cs └── UpdateOrderCommandHandler.cs 📁 Queries/ 📁 GetOrderById/ ├── GetOrderByIdQuery.cs ├── GetOrderByIdQueryHandler.cs └── OrderDto.cs 📁 GetAllOrders/ ├── GetAllOrdersQuery.cs └── GetAllOrdersQueryHandler.cs 📁 Common/ 📁 Behaviors/ ├── ValidationBehavior.cs ├── LoggingBehavior.cs └── PerformanceBehavior.cs 📁 Domain/ 📁 Entities/ └── Order.cs 📁 Events/ └── OrderCreatedEvent.cs 📁 Infrastructure/ 📁 Persistence/ └── OrderRepository.cs 📁 WebApi/ 📁 Controllers/ └── OrdersController.cs
record در C# 9+ استفاده کن💡 نکته مهم: اگه داری روی یه پروژه از صفر شروع میکنی، پیشنهاد میکنم ابتدا با معماری سادهتر شروع کنی و وقتی پروژه بزرگتر شد و نیاز واقعی به CQRS دیدی، اون موقع Refactor کنی. این رویکرد pragmatic از Over-Engineering جلوگیری میکنه.
در این آموزش، با الگوی CQRS در دات نت به صورت کامل آشنا شدیم. یاد گرفتیم که CQRS چیست، چرا لازمه، چطور با MediatR پیادهسازی میشه و چه زمانی باید ازش استفاده کرد.
مهمترین چیزی که باید به خاطر بسپاری اینه که CQRS یه ابزاره، نه یه هدف. اون رو وقتی استفاده کن که واقعاً به حل مشکلت کمک میکنه، نه صرفاً برای اینکه مد روزه.
دقیقاً همونطور که یه معماری خوب مثل CQRS به کدنویست نظم و قدرت میده، سئوی حرفهای هم به کسبوکار آنلاینت نظم و قدرت میده. ما با تجربهای که در سئوی وب فارسی داریم، میتونیم سایتت رو به جایگاه شایستهاش در گوگل برسونیم.
آیا میخواهید سایت شما هم مثل رقبا در صفحه اول گوگل باشد و زنگخورهایتان چند برابر شود؟ سئوی سایت خود را به متخصصان ما بسپارید.
همین حالا برای مشاوره رایگان با ما تماس بگیرید:
📞 09190994063 - 09376846692
نه، این دو مفهوم متفاوت هستند. Clean Architecture یه سبک معماری کلیه که لایههای مختلف برنامه رو از هم جدا میکنه. CQRS یه الگوی طراحی است که میتونه داخل Clean Architecture یا هر معماری دیگهای پیادهسازی بشه. اما در عمل خیلی وقتها این دو تا رو با هم و در کنار MediatR استفاده میکنن.
بله، به طور کلی CQRS وقتی ارزش واقعیش رو نشون میده که پروژه منطق تجاری پیچیده یا نیاز به مقیاسپذیری بالا داشته باشه. برای پروژههای کوچیک، این الگو ممکنه Overhead غیرضروری ایجاد کنه. البته خیلی از توسعهدهندهها حتی برای پروژههای متوسط هم از CQRS با MediatR استفاده میکنن چون کد رو منظمتر و تستپذیرتر میکنه.
CQS (Command Query Separation) یه اصل طراحی در سطح متد است که میگه یه متد یا باید فقط داده برگردونه (Query) یا فقط وضعیت رو تغییر بده (Command)، نه هر دو. CQRS این مفهوم رو در سطح کل معماری سیستم اعمال میکنه و مدلهای جداگانهای برای Read و Write ایجاد میکنه.
بهترین روش اینه که به تدریج (Incrementally) این کار رو انجام بدی. اول کتابخانه MediatR رو نصب کن. بعد یکی از Use Caseهای جدید رو به جای روش قدیمی با CQRS بنویس. کمکم بخشهای دیگه رو هم Refactor کن. هیچوقت سعی نکن یهشبه همه چیز رو تغییر بدی چون ریسک بالایی داره.
خیر، این یه سوءتفاهم رایجه. داشتن دو دیتابیس جداگانه برای Read و Write فقط یکی از پیادهسازیهای ممکن CQRS است، نه یه الزام. اکثر پروژهها با یه دیتابیس واحد شروع میکنن و فقط مدلهای منطقی Read و Write رو جدا میکنن. جداسازی فیزیکی دیتابیسها فقط وقتی منطقیه که نیاز واقعی به مقیاسپذیری مستقل وجود داشته باشه.
محبوبترین پکیجها عبارتند از: MediatR (برای پیادهسازی الگوی Mediator)، FluentValidation (برای اعتبارسنجی Commandها)، AutoMapper (برای تبدیل Entity به DTO)، و Scrutor (برای ثبت خودکار Handlerها در DI Container). ترکیب این چهار پکیج یه پایه خیلی قوی برای CQRS در ASP.NET Core ایجاد میکنه.