عندما يتوقف CRUD العادي عن العمل: مقدمة إلى CQRS و Event Sourcing
\n\nتخيل أنك تقوم بكتابة نظام لبنك. كل ثانية، مئات المستخدمين يتحققون من الرصيد، ويحولون الأموال، ويفتحون الودائع. تطبيقك على C# يعمل على Entity Framework القياسي مع عمليات CRUD النموذجية. وفجأة - تتعطل قاعدة البيانات. لماذا؟ لأن تقريرًا ثقيلًا واحدًا قام بحظر الجدول أثناء كتابة المعاملة. هل هذا مألوف؟
مطورو الأنظمة المعقدة على C# عاجلاً أم آجلاً يصطدمون بسقف البنية الكلاسيكية. تأتي المساعدة من نمطين قويين: CQRS (فصل مسؤولية الأوامر والاستعلامات) و Event Sourcing. دعنا نحللهما عمليًا - بأمثلة حية، وليس بنظرية جافة.
\n\nما هو CQRS ولماذا هو ليس مجرد "تقسيم"؟
\n\nCQRS ليس حول وضع الطرق في مجلدات مختلفة. إنها فلسفة: الأوامر (تغير الحالة) و الاستعلامات (تقرأ الحالة) يجب أن تعيش بشكل منفصل. في CRUD الكلاسيكي، لديك نموذج بيانات واحد لكل من القراءة والكتابة. في CQRS، لديك على الأقل اثنان.
مشكلة النهج الكلاسيكي
\nلنفترض أن لديك UserRepository مع طريقة GetUserById. عندما تقرأ المستخدم لعرضه على الشاشة، تحتاج إلى 3 حقول. عندما تقوم بالتحديث - 10 حقول وتحقق. ونتيجة لذلك، يتحول النموذج الواحد إلى وحش يحتوي على حقول null ومخططات تعيين مربكة.
في CQRS، تقوم بفصل المسؤولية:
\n- \n
- Command - ينفذ إجراءً (مثل
CreateOrderCommand) \n - Query - يعيد البيانات (مثل
GetOrderQuery) \n - Read Model - DTO مسطح، مُحسَّن لشاشة معينة \n
- Write Model - نموذج مجال غني بمنطق الأعمال \n
كتابة CQRS على C# باستخدام MediatR
\nالطريقة الأكثر شيوعًا لتنفيذ CQRS في C# هي استخدام مكتبة MediatR. وهي تنفذ نمط Mediator وتتناسب تمامًا مع مفهوم الأوامر والاستعلامات.
الخطوة 1: تعريف الأمر
\npublic record CreateOrderCommand(\n Guid CustomerId,\n List<OrderItemDto> Items,\n string ShippingAddress\n) : IRequest<Guid>; // نعيد معرف الطلب الذي تم إنشاؤه\n\nالخطوة 2: معالج الأمر
\npublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>\n{\n private readonly IOrderRepository _orderRepository;\n private readonly IUnitOfWork _unitOfWork;\n\n public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)\n {\n _orderRepository = orderRepository;\n _unitOfWork = unitOfWork;\n }\n\n public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)\n {\n // منطق الأعمال: التحقق، المصادقة، الإنشاء\n var order = new Order(request.CustomerId, request.ShippingAddress);\n \n foreach (var item in request.Items)\n {\n order.AddItem(item.ProductId, item.Quantity, item.Price);\n }\n\n await _orderRepository.AddAsync(order);\n await _unitOfWork.SaveChangesAsync(cancellationToken);\n \n return order.Id;\n }\n}\n\nالخطوة 3: تعريف الاستعلام
\npublic record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;\n\nالخطوة 4: معالج الاستعلام (يقرأ من قاعدة بيانات منفصلة!)
\npublic class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>\n{\n private readonly IReadDbContext _readDbContext; // سياق منفصل للقراءة\n\n public GetOrderSummaryQueryHandler(IReadDbContext read