---
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