14 KiB
14 KiB
name, description, compatibility, metadata
| name | description | compatibility | metadata | ||||
|---|---|---|---|---|---|---|---|
| cqrs-mediatr | CQRS pattern với MediatR. Use for Commands, Queries, Handlers, Pipeline Behaviors, và Idempotency. | .NET 10+, MediatR 12+, FluentValidation |
|
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
/// <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
/// <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
/// <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
/// <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
/// <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
/// <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
/// <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
// ❌ 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
// ❌ 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
// ❌ 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
// 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
// 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 - Full code examples
- Repository Pattern - Data access
- Error Handling - Validation errors
- Testing Patterns - Handler testing
- API Design - Controller patterns