در این مقاله تخصصی، به بررسی عمیق اصول CQRS، نحوه پیادهسازی عملیاتی آن در اکوسیستم داتنت (با تمرکز بر MediatR و Entity Framework Core)، چالشهای همگامسازی داده و پیادهسازی Eventual Consistency میپردازیم.
در معماریهای سنتی CRUD، یک DbContext یا یک Data Model واحد مسئولیت هر دو عملیات خواندن و نوشتن را بر عهده دارد. این رویکرد در برنامههای کوچک بسیار سریع و ساده است، اما با رشد سیستم چالشهای زیر رخ میدهند:
تداخل بهینهسازی (Optimization Mismatch): برای عملیات نوشتن، به مدلهای نرمالشده (Normalized) با اعتبارسنجیهای پیچیده و حفظ یکپارچگی داده نیاز داریم. اما برای خواندن، به دادههای غیرنرمال (Denormalized)، مسطح (Flat) و Aggregated برای نمایش سریع در UI نیازمندیم. یک مدل واحد نمیتواند هر دو کار را به بهینهترین شکل انجام دهد.
قفلشدگی و عملکرد (Locking & Contention): تراکنشهای سنگین نوشتن (Write Locks) بر روی جداول بانک اطلاعاتی مستقیم روی سرعت Executing Queryهای خواندن تاثیر منفی میگذارند.
پیچیدگی Security & Permissions: کنترل دسترسی در سطح فیلدها برای خواندن و نوشتن روی یک مدل یکسان، کد را مملو از if/elseهای پیچیده میکند.
CQRS با جدا کردن این دو مسیر، امکان مقیاسپذیری مستقل (Independent Scaling) و سادهسازی Domain Model را فراهم میسازد.
قبل از کدنویسی، باید تفاوت ماهوی این دو مفهوم را درک کنیم:
| ویژگی | Command (دستور) | Query (پرسوجو) |
| هدف | تغییر در وضعیت سیستم (State Change) | خواندن دادهها بدون تغییر وضعیت |
| خروجی | معمولاً void یا یک شناسه (Guid/int) یا نتیجه Success/Failure | DTO یا Data Model اختصاصی خواندن |
| عوارض جانبی (Side Effects) | دارد | ندارد (Idempotent و Read-Only) |
| اعتبارسنجی (Validation) | شامل Business Rules و Domain Validation | عمدتاً محدود به Input Format |
کتابخانه MediatR پیادهسازی الگوی Mediator در داتنت است که decoupling بهینهای میان ارسالکننده درخواست و پردازنده آن (Handler) ایجاد میکند. این کتابخانه بهترین ابزار برای پیادهسازی CQRS در یک برنامه .NET به شمار میرود.
ساختار پروژهای پیشنهادی (Clean Architecture)
src/
├── Core/
│ ├── Application/
│ │ ├── Products/
│ │ │ ├── Commands/
│ │ │ │ ├── CreateProduct/
│ │ │ │ │ ├── CreateProductCommand.cs
│ │ │ │ │ ├── CreateProductCommandHandler.cs
│ │ │ │ │ └── CreateProductCommandValidator.cs
│ │ │ └── Queries/
│ │ │ ├── GetProductById/
│ │ │ │ ├── GetProductByIdQuery.cs
│ │ │ │ ├── GetProductByIdQueryHandler.cs
│ │ │ │ └── ProductDetailsDto.cs
├── Infrastructure/
│ └── Persistence/
│ ├── ApplicationDbContext.cs (Write Side - EF Core)
│ └── DapperContext.cs (Read Side - High Performance)
└── Presentation/
└── WebApi/
در لایه نوشتن، تمرکز بر روی رفتار Domain، حفظ Transaction integrity و اعتبارسنجی قوانین کسبوکار است. در اینجا از EF Core استفاده میکنیم.
تعریف Command و Handler
using MediatR;
namespace Application.Products.Commands.CreateProduct;
// ۱. تعریف Command به عنوان یک Record غیرقابل تغییر (Immutable)
public record CreateProductCommand(
string Name,
decimal Price,
int StockQuantity
) : IRequest;
using Domain.Entities;
using Infrastructure.Persistence;
using MediatR;
namespace Application.Products.Commands.CreateProduct;
// ۲. پیادهسازی Command Handler
public class CreateProductCommandHandler : IRequestHandler
{
private readonly ApplicationDbContext _dbContext;
public CreateProductCommandHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
// اعمال قوانین کسبوکار در Domain Model
var product = new Product(request.Name, request.Price, request.StockQuantity);
_dbContext.Products.Add(product);
await _dbContext.SaveChangesAsync(cancellationToken);
return product.Id;
}
}
اعتبارسنجی متقاطع با FluentValidation و MediatR Pipeline Behavior
یکی از نقاط قوت MediatR امکان استفاده از Pipeline Behaviors برای پیادهسازی Cross-cutting Concernها نظیر Validation و Logging است:
using FluentValidation;
namespace Application.Products.Commands.CreateProduct;
public class CreateProductCommandValidator : AbstractValidator
{
public CreateProductCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("نام محصول الزامی است.")
.MaximumLength(200);
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("قیمت باید بزرگتر از صفر باشد.");
RuleFor(x => x.StockQuantity)
.GreaterThanOrEqualTo(0);
}
}
پیادهسازی Validation Behavior برای اجرای خودکار اعتبارسنجیها قبل از رسیدن درخواست به Handler:
using FluentValidation;
using MediatR;
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())
{
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();
}
}
در سمت Query، هدف اصلی سرعت بالا و عدم تحمیل Overhead از سمت Change Tracker یا Domain Model است. میتوان از EF Core با ویژگی AsNoTracking() یا ORMهای سبکی مانند Dapper استفاده کرد.
تعریف Query و DTO
using MediatR;
namespace Application.Products.Queries.GetProductById;
public record ProductDetailsDto(Guid Id, string Name, decimal Price, int StockQuantity);
public record GetProductByIdQuery(Guid Id) : IRequest;
پیادهسازی Query Handler با Dapper برای حداکثر کارایی
using System.Data;
using Dapper;
using MediatR;
namespace Application.Products.Queries.GetProductById;
public class GetProductByIdQueryHandler : IRequestHandler
{
private readonly IDbConnection _dbConnection;
public GetProductByIdQueryHandler(IDbConnection dbConnection)
{
_dbConnection = dbConnection;
}
public async Task Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
{
const string sql = @"
SELECT Id, Name, Price, StockQuantity
FROM Products
WHERE Id = @Id";
// خواندن مستقیم و بدون Tracking جهت افزایش کارایی
return await _dbConnection.QueryFirstOrDefaultAsync(
sql,
new { Id = request.Id }
);
}
}
سطوح پیادهسازی CQRS متغیر است:
CQRS درونبرنامهای (In-Memory CQRS): یک دیتابیس مشترک، اما تفکیک کلاسهای Command و Query در سطح کد.
CQRS با دیتابیسهای مجزا (Database-level CQRS): یک دیتابیس Relational (مانند SQL Server) برای نوشتن و یک دیتابیس NoSQL (مانند ElasticSearch یا MongoDB) برای خواندن.
همگامسازی دادهها (Data Synchronization) با Domain Events
هنگامی که دیتابیسهای خواندن و نوشتن را جدا میکنیم، تغییرات دیتابیس نوشتن باید به دیتابیس خواندن منتقل شوند. این کار معمولاً از طریق Domain Events و الگوی Outbox Pattern انجام میشود.
// ۱. تعریف Domain Event
public record ProductCreatedEvent(Guid Id, string Name, decimal Price) : INotification;
// ۲. انتشار Event در Command Handler یا Entity
public class Product : AggregateRoot
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public Product(string name, decimal price, int stock)
{
Id = Guid.NewGuid();
Name = name;
Price = price;
// ثبت event جهت انتشار پس از SaveChanges
AddDomainEvent(new ProductCreatedEvent(Id, Name, Price));
}
}
// ۳. Handler جهت بهروزرسانی بانک اطلاعاتی Read Side (مثلاً MongoDB یا Read Model)
public class ProductCreatedEventHandler : INotificationHandler
{
private readonly IReadRepository _readRepository;
public ProductCreatedEventHandler(IReadRepository readRepository)
{
_readRepository = readRepository;
}
public async Task Handle(ProductCreatedEvent notification, CancellationToken cancellationToken)
{
var readModel = new ProductReadModel
{
Id = notification.Id,
Name = notification.Name,
Price = notification.Price
};
await _readRepository.InsertAsync(readModel, cancellationToken);
}
}
نکته کلیدی (Eventual Consistency): در دیتابیسهای مجزا، همگامسازی بلافاصله انجام نمیشود بلکه سیستم دچار «سازگاری تدریجی» یا Eventual Consistency است. برای جلوگیری از بروز خطا، باید الگوی Outbox و Message Brokerهایی مثل MassTransit یا RabbitMQ را جهت تضمین ارسال پیامها (At-least-once delivery) به کار گرفت.
در داتنت، استفاده از Minimal APIs به همراه MediatR ساختاری شفاف و خلوت ایجاد میکند:
var builder = WebApplication.CreateBuilder(args);
// ثبت خدمات MediatR و Validators
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
var app = builder.Build();
// Endpoint مربوط به Command
app.MapPost("/products", async (CreateProductCommand command, ISender mediator) =>
{
var productId = await mediator.Send(command);
return Results.Created($"/products/{productId}", productId);
});
// Endpoint مربوط به Query
app.MapGet("/products/{id:guid}", async (Guid id, ISender mediator) =>
{
var product = await mediator.Send(new GetProductByIdQuery(id));
return product is not null ? Results.Ok(product) : Results.NotFound();
});
app.Run();
مزایا، معایب و Anti-Patternها
مزایا
Single Responsibility Principle: هر کلاس فقط یک مسئولیت مشخص دارد.
Scalability بالا: امکان مقیاسبندی جداگانه سرورهای Read و Write.
بهینهسازی Queryها: آزادی کامل در نوشتن SQLهای پیچیده و استفاده از Dapper بدون آسیب به Domain Model.
تستپذیری فوقالعاده: تست Unit برای Handlerها به دلیل ورودی و خروجی مشخص بسیار ساده است.
معایب و چالشها
افزایش Complexity و تعداد فایلها: برای یک عملیات ساده باید چندین کلاس (Command, Handler, DTO, Validator) تعریف شود.
مدیریت Eventual Consistency: پیچیدگی در هندل کردن تاخیر همگامسازی دیتابیسها و عدم وجود ACID Transaction یکپارچه در سطح دو دیتابیس.
Anti-Patternهای رایج
استفاده از CQRS برای برنامههای ساده (Over-engineering): برای برنامههای کوچک با منطق کسبوکار ساده، CRUD سنتی گزینه مناسبتری است.
ارسال Entityهای EF Core در Query Response: همیشه خروجی Query باید DTOهای مخصوص خواندن باشد، نه Entityهای لایه Domain.
تغییر وضعیت سیستم در Queryها: یک Query هرگز نباید وضعیت دیتابیس یا سیستم را تغییر دهد.
الگوی CQRS یک نقشه راه قدرتمند برای توسعه سیستمهای بزرگ، مقیاسپذیر و قابل نگهداری در داتنت است. با جداسازی مسیرهای Read و Write و بهرهگیری از ابزارهایی نظیر MediatR، EF Core و Dapper، میتوانید معماری نرمافزار خود را طوری طراحی کنید که در برابر حجم بالای درخواستها و پیچیدگیهای روزافزون کسبوکار به راحتی پاسخگو باشد. کلید موفقیت در استفاده از این الگو، ارزیابی درست نیازمندیهای پروژه و جلوگیری از Over-engineering در سیستمهای ساده است.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.