455 lines
14 KiB
Markdown
455 lines
14 KiB
Markdown
---
|
|
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
|
|
/// <summary>
|
|
/// EN: Command to create a new order.
|
|
/// VI: Command tạo order mới.
|
|
/// </summary>
|
|
public record CreateOrderCommand(
|
|
string UserId,
|
|
Address ShippingAddress,
|
|
List<OrderItemDto> Items) : IRequest<OrderResult>;
|
|
|
|
/// <summary>
|
|
/// EN: Command result.
|
|
/// VI: Kết quả command.
|
|
/// </summary>
|
|
public record OrderResult(Guid OrderId);
|
|
```
|
|
|
|
### Query Definition / Định Nghĩa Query
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Query to get user orders.
|
|
/// VI: Query lấy orders của user.
|
|
/// </summary>
|
|
public record GetUserOrdersQuery(
|
|
string UserId,
|
|
int Skip = 0,
|
|
int Take = 20) : IRequest<PagedResult<OrderSummaryDto>>;
|
|
|
|
/// <summary>
|
|
/// EN: Lightweight DTO for query results.
|
|
/// VI: DTO nhẹ cho kết quả query.
|
|
/// </summary>
|
|
public record OrderSummaryDto(
|
|
Guid Id,
|
|
string Status,
|
|
decimal TotalAmount,
|
|
DateTime CreatedAt,
|
|
int ItemCount);
|
|
```
|
|
|
|
### Command Handler / Handler Command
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Handler for CreateOrderCommand.
|
|
/// VI: Handler cho CreateOrderCommand.
|
|
/// </summary>
|
|
public class CreateOrderCommandHandler
|
|
: IRequestHandler<CreateOrderCommand, OrderResult>
|
|
{
|
|
private readonly IOrderRepository _orderRepository;
|
|
private readonly ILogger<CreateOrderCommandHandler> _logger;
|
|
|
|
public CreateOrderCommandHandler(
|
|
IOrderRepository orderRepository,
|
|
ILogger<CreateOrderCommandHandler> logger)
|
|
{
|
|
_orderRepository = orderRepository;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<OrderResult> 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
|
|
/// <summary>
|
|
/// EN: Query handler using Dapper for optimized reads.
|
|
/// VI: Query handler dùng Dapper cho đọc tối ưu.
|
|
/// </summary>
|
|
public class GetUserOrdersQueryHandler
|
|
: IRequestHandler<GetUserOrdersQuery, PagedResult<OrderSummaryDto>>
|
|
{
|
|
private readonly IDbConnection _connection;
|
|
|
|
public GetUserOrdersQueryHandler(IDbConnection connection)
|
|
{
|
|
_connection = connection;
|
|
}
|
|
|
|
public async Task<PagedResult<OrderSummaryDto>> Handle(
|
|
GetUserOrdersQuery request,
|
|
CancellationToken ct)
|
|
{
|
|
const string countSql = "SELECT COUNT(*) FROM Orders WHERE UserId = @UserId";
|
|
var total = await _connection.ExecuteScalarAsync<int>(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<OrderSummaryDto>(sql, new
|
|
{
|
|
request.UserId,
|
|
request.Skip,
|
|
request.Take
|
|
});
|
|
|
|
return new PagedResult<OrderSummaryDto>(orders.ToList(), total);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Validation Behavior / Behavior Validation
|
|
|
|
```csharp
|
|
/// <summary>
|
|
/// EN: Pipeline behavior for FluentValidation.
|
|
/// VI: Pipeline behavior cho FluentValidation.
|
|
/// </summary>
|
|
public class ValidationBehavior<TRequest, TResponse>
|
|
: IPipelineBehavior<TRequest, TResponse>
|
|
where TRequest : IRequest<TResponse>
|
|
{
|
|
private readonly IEnumerable<IValidator<TRequest>> _validators;
|
|
|
|
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
|
|
{
|
|
_validators = validators;
|
|
}
|
|
|
|
public async Task<TResponse> Handle(
|
|
TRequest request,
|
|
RequestHandlerDelegate<TResponse> next,
|
|
CancellationToken ct)
|
|
{
|
|
if (!_validators.Any())
|
|
return await next();
|
|
|
|
var context = new ValidationContext<TRequest>(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
|
|
/// <summary>
|
|
/// EN: Pipeline behavior for logging.
|
|
/// VI: Pipeline behavior cho logging.
|
|
/// </summary>
|
|
public class LoggingBehavior<TRequest, TResponse>
|
|
: IPipelineBehavior<TRequest, TResponse>
|
|
where TRequest : IRequest<TResponse>
|
|
{
|
|
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
|
|
|
|
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<TResponse> Handle(
|
|
TRequest request,
|
|
RequestHandlerDelegate<TResponse> 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
|
|
/// <summary>
|
|
/// EN: Slim controller using MediatR.
|
|
/// VI: Controller gọn nhẹ với MediatR.
|
|
/// </summary>
|
|
[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<ActionResult<ApiResponse<OrderResult>>> 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<OrderResult>.Ok(result));
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<ApiResponse<PagedResult<OrderSummaryDto>>>> 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<PagedResult<OrderSummaryDto>>.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<IEnumerable<Order>> 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<IEnumerable<OrderDto>> Handle(GetOrdersQuery query, CancellationToken ct)
|
|
{
|
|
const string sql = "SELECT Id, Status, TotalAmount FROM Orders WHERE UserId = @UserId";
|
|
return await _connection.QueryAsync<OrderDto>(sql, new { query.UserId });
|
|
}
|
|
```
|
|
|
|
### 2. Missing Pipeline Behaviors
|
|
|
|
```csharp
|
|
// ❌ BAD: Validation in handler
|
|
public async Task<OrderResult> 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<IActionResult> 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<IActionResult> 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<T>` | 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
|