--- name: cqrs-mediatr description: CQRS pattern với MediatR. Use for Commands, Queries, Handlers, Pipeline Behaviors, và Idempotency. compatibility: ".NET 10+, MediatR 12+, FluentValidation" metadata: author: Velik Ho version: "1.0" --- # CQRS & MediatR Patterns / Mẫu CQRS & MediatR CQRS pattern với MediatR cho GoodGo microservices. ## When to Use This Skill / Khi Nào Sử Dụng Use this skill when: - Separating read/write operations / Tách biệt operations đọc/ghi - Creating Commands and Queries / Tạo Commands và Queries - Implementing MediatR handlers / Triển khai MediatR handlers - Adding cross-cutting concerns via Behaviors / Thêm behaviors xuyên suốt - Ensuring idempotency / Đảm bảo tính idempotent ## Core Concepts / Khái Niệm Cốt Lõi ### CQRS Overview / Tổng Quan CQRS ``` ┌─────────────────────────────────────────────────────────────┐ │ API Controller │ └─────────────────────┬───────────────────────────┬───────────┘ │ │ ┌───────▼───────┐ ┌───────▼───────┐ │ Commands │ │ Queries │ │ (Write/Ghi) │ │ (Read/Đọc) │ └───────┬───────┘ └───────┬───────┘ │ │ ┌───────▼───────┐ ┌───────▼───────┐ │ MediatR │ │ MediatR │ │ Handler │ │ Handler │ └───────┬───────┘ └───────┬───────┘ │ │ ┌───────▼───────┐ ┌───────▼───────┐ │ Domain Model │ │ Dapper │ │ EF Core │ │ Raw SQL │ └───────┬───────┘ └───────┬───────┘ │ │ └───────────────┬───────────────────┘ │ ┌────────▼────────┐ │ Database │ └─────────────────┘ ``` ### Command vs Query / Lệnh vs Truy Vấn | Aspect | Command | Query | |--------|---------|-------| | **Purpose** | Modify state | Read data | | **Returns** | Result/void | Data/DTO | | **Model** | Full Domain Model | Lightweight DTO | | **ORM** | EF Core | Dapper/Raw SQL | | **Validation** | Full business rules | Minimal | ### MediatR Flow / Luồng MediatR ``` Request → Pipeline Behaviors → Handler → Response ├── LoggingBehavior ├── ValidationBehavior └── TransactionBehavior ``` ## Key Patterns / Mẫu Chính ### Command Definition / Định Nghĩa Command ```csharp /// /// EN: Command to create a new order. /// VI: Command tạo order mới. /// public record CreateOrderCommand( string UserId, Address ShippingAddress, List Items) : IRequest; /// /// EN: Command result. /// VI: Kết quả command. /// public record OrderResult(Guid OrderId); ``` ### Query Definition / Định Nghĩa Query ```csharp /// /// EN: Query to get user orders. /// VI: Query lấy orders của user. /// public record GetUserOrdersQuery( string UserId, int Skip = 0, int Take = 20) : IRequest>; /// /// EN: Lightweight DTO for query results. /// VI: DTO nhẹ cho kết quả query. /// public record OrderSummaryDto( Guid Id, string Status, decimal TotalAmount, DateTime CreatedAt, int ItemCount); ``` ### Command Handler / Handler Command ```csharp /// /// EN: Handler for CreateOrderCommand. /// VI: Handler cho CreateOrderCommand. /// public class CreateOrderCommandHandler : IRequestHandler { private readonly IOrderRepository _orderRepository; private readonly ILogger _logger; public CreateOrderCommandHandler( IOrderRepository orderRepository, ILogger logger) { _orderRepository = orderRepository; _logger = logger; } public async Task Handle( CreateOrderCommand request, CancellationToken ct) { // EN: Create order through domain model // VI: Tạo order qua domain model var order = new Order(request.UserId, request.ShippingAddress); foreach (var item in request.Items) { order.AddItem(item.ProductId, item.Quantity, item.UnitPrice); } await _orderRepository.AddAsync(order, ct); await _orderRepository.UnitOfWork.SaveChangesAsync(ct); _logger.LogInformation( "EN: Order created / VI: Order đã tạo: {OrderId}", order.Id); return new OrderResult(order.Id); } } ``` ### Query Handler with Dapper / Handler Query với Dapper ```csharp /// /// EN: Query handler using Dapper for optimized reads. /// VI: Query handler dùng Dapper cho đọc tối ưu. /// public class GetUserOrdersQueryHandler : IRequestHandler> { private readonly IDbConnection _connection; public GetUserOrdersQueryHandler(IDbConnection connection) { _connection = connection; } public async Task> Handle( GetUserOrdersQuery request, CancellationToken ct) { const string countSql = "SELECT COUNT(*) FROM Orders WHERE UserId = @UserId"; var total = await _connection.ExecuteScalarAsync(countSql, new { request.UserId }); const string sql = @" SELECT o.Id, o.Status, o.TotalAmount, o.CreatedAt, COUNT(oi.Id) as ItemCount FROM Orders o LEFT JOIN OrderItems oi ON o.Id = oi.OrderId WHERE o.UserId = @UserId GROUP BY o.Id, o.Status, o.TotalAmount, o.CreatedAt ORDER BY o.CreatedAt DESC OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY"; var orders = await _connection.QueryAsync(sql, new { request.UserId, request.Skip, request.Take }); return new PagedResult(orders.ToList(), total); } } ``` ### Validation Behavior / Behavior Validation ```csharp /// /// EN: Pipeline behavior for FluentValidation. /// VI: Pipeline behavior cho FluentValidation. /// 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 ct) { if (!_validators.Any()) return await next(); var context = new ValidationContext(request); var failures = (await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, ct)))) .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); if (failures.Count != 0) throw new ValidationException(failures); return await next(); } } ``` ### Logging Behavior / Behavior Logging ```csharp /// /// EN: Pipeline behavior for logging. /// VI: Pipeline behavior cho logging. /// public class LoggingBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ILogger> _logger; public LoggingBehavior(ILogger> logger) { _logger = logger; } public async Task Handle( TRequest request, RequestHandlerDelegate next, CancellationToken ct) { var requestName = typeof(TRequest).Name; _logger.LogInformation( "EN: Handling {RequestName} / VI: Xử lý {RequestName}", requestName); var stopwatch = Stopwatch.StartNew(); var response = await next(); stopwatch.Stop(); _logger.LogInformation( "EN: Handled {RequestName} in {ElapsedMs}ms / VI: Đã xử lý {RequestName} trong {ElapsedMs}ms", requestName, stopwatch.ElapsedMilliseconds); return response; } } ``` ### Controller with MediatR / Controller với MediatR ```csharp /// /// EN: Slim controller using MediatR. /// VI: Controller gọn nhẹ với MediatR. /// [ApiController] [Route("api/v{version:apiVersion}/orders")] public class OrdersController : ControllerBase { private readonly IMediator _mediator; public OrdersController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task>> CreateOrder( CreateOrderRequest request, CancellationToken ct) { var command = new CreateOrderCommand( GetUserId(), request.ShippingAddress, request.Items); var result = await _mediator.Send(command, ct); return CreatedAtAction( nameof(GetOrder), new { orderId = result.OrderId }, ApiResponse.Ok(result)); } [HttpGet] public async Task>>> GetOrders( [FromQuery] int skip = 0, [FromQuery] int take = 20, CancellationToken ct = default) { var query = new GetUserOrdersQuery(GetUserId(), skip, take); var result = await _mediator.Send(query, ct); return Ok(ApiResponse>.Ok(result)); } private string GetUserId() => User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new UnauthorizedAccessException(); } ``` ## Common Mistakes / Lỗi Thường Gặp ### 1. Using Domain Model for Queries ```csharp // ❌ BAD: Using EF Core for simple reads public async Task> Handle(GetOrdersQuery query, CancellationToken ct) { return await _context.Orders .Include(o => o.OrderItems) .Where(o => o.UserId == query.UserId) .ToListAsync(ct); } // ✅ GOOD: Using Dapper for optimized reads public async Task> Handle(GetOrdersQuery query, CancellationToken ct) { const string sql = "SELECT Id, Status, TotalAmount FROM Orders WHERE UserId = @UserId"; return await _connection.QueryAsync(sql, new { query.UserId }); } ``` ### 2. Missing Pipeline Behaviors ```csharp // ❌ BAD: Validation in handler public async Task Handle(CreateOrderCommand request, CancellationToken ct) { if (string.IsNullOrEmpty(request.UserId)) throw new ValidationException("UserId required"); // ... } // ✅ GOOD: Validation via Behavior builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(Program).Assembly); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); }); ``` ### 3. Fat Controllers ```csharp // ❌ BAD: Logic in controller [HttpPost] public async Task CreateOrder(CreateOrderRequest request) { var order = new Order(request.UserId, request.Address); foreach (var item in request.Items) order.AddItem(item.ProductId, item.Quantity, item.UnitPrice); await _repository.AddAsync(order); await _repository.UnitOfWork.SaveChangesAsync(); return Ok(order.Id); } // ✅ GOOD: Delegate to MediatR [HttpPost] public async Task CreateOrder(CreateOrderRequest request) { var command = request.ToCommand(GetUserId()); var result = await _mediator.Send(command); return CreatedAtAction(nameof(GetOrder), new { orderId = result.OrderId }, result); } ``` ## Quick Reference / Tham Chiếu Nhanh ### MediatR Request Types | Interface | Purpose | Example | |-----------|---------|---------| | `IRequest` | Request with response | Commands, Queries | | `IRequest` | Request without response | Fire-and-forget | | `INotification` | Event notification | Domain events | ### Pipeline Behavior Order ```csharp // EN: Registration order = execution order // VI: Thứ tự đăng ký = thứ tự thực thi cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); // 1st cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // 2nd cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>)); // 3rd ``` ### DI Registration ```csharp // EN: Register MediatR with behaviors // VI: Đăng ký MediatR với behaviors builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(Program).Assembly); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); }); // EN: Register FluentValidation // VI: Đăng ký FluentValidation builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly); ``` ## Resources / Tài Nguyên - [Detailed Examples](./references/REFERENCE.md) - Full code examples - [Repository Pattern](../repository-pattern/SKILL.md) - Data access - [Error Handling](../error-handling-patterns/SKILL.md) - Validation errors - [Testing Patterns](../testing-patterns/SKILL.md) - Handler testing - [API Design](../api-design/SKILL.md) - Controller patterns