CQRS và MediatR bị overused trong .NET community. Nhiều project dùng MediatR chỉ vì "mọi người đều dùng", nhưng không hiểu tại sao. Session này giúp bạn hiểu bản chất để biết khi nào dùng, khi nào KHÔNG.
Mục tiêu
- Hiểu CQRS là gì thật sự (không phải chỉ "tách read/write")
- MediatR giải quyết vấn đề gì, vấn đề gì KHÔNG giải quyết
- Khi nào CQRS là overkill
- Pipeline Behaviors - giá trị thực
1) CQRS - Bản Chất
Câu hỏi đầu tiên: Tại sao tách Command và Query?
Vì Read và Write có yêu cầu KHÁC NHAU:
Write (Command):
- Cần validation phức tạp
- Cần business rules (domain model)
- Cần consistency (ACID transaction)
- Throughput: thấp hơn
- Model: normalized, rich entities
Read (Query):
- Cần performance cao
- Không cần business rules
- Có thể denormalized
- Throughput: cao hơn (>90% traffic là reads)
- Model: flat DTOs, optimized cho UI
→ Dùng CÙNG 1 model cho cả 2 = compromise cả 2 bên
→ Tách model = optimize RIÊNG mỗi bên
CQRS có 3 levels
Level 1: Tách model (phổ biến nhất, hầu hết dùng level này)
Commands → Domain Entities (EF Core, rich model)
Queries → DTOs (Dapper, projections, denormalized views)
→ CÙNG 1 database
Level 2: Tách database
Commands → Write DB (normalized SQL)
Queries → Read DB (denormalized, read replica, Redis)
→ Eventual consistency giữa 2 DB
Level 3: Event Sourcing
Commands → Event Store (lưu events)
Queries → Read Projections (materialized views từ events)
→ Phức tạp nhất, hiếm khi cần
80% projects chỉ cần Level 1. Đừng nhảy sang Level 2/3 trừ khi performance/scale thực sự đòi hỏi.
2) MediatR - Giải Quyết Gì?
csharp// KHÔNG có MediatR:
public class OrdersController
{
private readonly IOrderService _orderService;
private readonly IProductService _productService;
private readonly IEmailService _emailService;
private readonly ILogger _logger;
private readonly IValidator _validator;
// ... 10+ dependencies
public async Task<IActionResult> Create(CreateOrderRequest req)
{
// Validation...
// Logging...
// Business logic...
// Send email...
// → Controller quá phình
}
}
// VỚI MediatR:
public class OrdersController(ISender mediator)
{
// 1 dependency duy nhất!
[HttpPost]
public async Task<IActionResult> Create(CreateOrderCommand cmd)
{
var result = await mediator.Send(cmd);
return result.IsSuccess ? Ok(result.Value) : BadRequest(result.Error);
}
}
// Logic nằm ở Handler (tách biệt):
public class CreateOrderHandler(
IOrderRepository repo,
IUnitOfWork uow)
: IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
CreateOrderCommand cmd, CancellationToken ct) { /* ... */ }
}
MediatR giải quyết:
✅ Decouple Controller khỏi business logic
✅ 1 handler = 1 use case (SRP)
✅ Pipeline Behaviors cho cross-cutting concerns
✅ Dễ test (test handler isolated)
MediatR KHÔNG giải quyết:
❌ Performance (thêm overhead: resolve handler, pipeline)
❌ Complexity (thêm files: Command, Handler, Validator, Response)
❌ "True" decoupling (Controller vẫn tạo Command object)
❌ Distributed messaging (MediatR là in-process, KHÔNG phải message broker)
3) Khi Nào CQRS/MediatR Là Overkill?
❌ OVERKILL khi:
- Simple CRUD (không có business logic)
- Small project (1-2 developers)
- Prototype/MVP (ship nhanh quan trọng hơn architecture)
- Read/Write models GIỐNG nhau (không cần tách)
✅ CÓ GIÁ TRỊ khi:
- Complex business logic (validation, rules, side effects)
- Team >3 developers (giảm merge conflicts, clear ownership)
- Pipeline behaviors hữu ích (validation, logging, transaction per use case)
- Read performance khác biệt (cần Dapper/denormalized cho reads)
Ví dụ: Khi nào CRUD đủ tốt?
csharp// Bảng Settings: 5 columns, không business logic
// → CRUD controller trực tiếp, KHÔNG cần MediatR
[HttpGet("{key}")]
public async Task<IActionResult> Get(string key)
{
var setting = await db.Settings.FindAsync(key);
return setting is null ? NotFound() : Ok(setting);
}
[HttpPut("{key}")]
public async Task<IActionResult> Update(string key, SettingDto dto)
{
var setting = await db.Settings.FindAsync(key);
if (setting is null) return NotFound();
setting.Value = dto.Value;
await db.SaveChangesAsync();
return NoContent();
}
// Simple, readable, 0 ceremony. MediatR ở đây = over-engineering.
4) Pipeline Behaviors - Giá Trị Thực
Pipeline: command đi qua CHUỖI behaviors trước khi đến handler
Request → [Logging] → [Validation] → [Transaction] → [Handler] → Response
↑ ↑ ↑
Ghi log Validate Begin/Commit
timing input transaction
csharp// Validation Behavior: validate MỌI command tự động
public class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
if (!validators.Any()) return await next();
var failures = validators
.Select(v => v.Validate(request))
.SelectMany(r => r.Errors)
.Where(e => e is not null)
.ToList();
if (failures.Count > 0)
throw new ValidationException(failures);
return await next();
}
}
// → Viết 1 lần, áp dụng cho MỌI command có validator
// → Không handler nào cần validate thủ công
// → ĐÂY LÀ GIÁ TRỊ LỚN NHẤT của MediatR pipeline!
Behaviors giá trị:
- Validation - tự động validate mọi command
- Logging - log timing + request data
- Transaction - wrap command trong DB transaction
- Caching - cache query results tự động
5) Alternatives: Khi Nào Không Dùng MediatR?
csharp// Alternative 1: Minimal APIs + handler methods (simple)
app.MapPost("/orders", async (CreateOrderRequest req, AppDbContext db) =>
{
var order = new Order { /* ... */ };
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/orders/{order.Id}", order);
});
// → Simple, fast, no ceremony. Tốt cho microservices nhỏ.
// Alternative 2: Service classes (truyền thống)
public class OrderService(AppDbContext db, IValidator<CreateOrderRequest> validator)
{
public async Task<Result<Guid>> CreateOrderAsync(CreateOrderRequest req)
{
var validation = await validator.ValidateAsync(req);
if (!validation.IsValid) return Result.Failure<Guid>(...);
// ...
}
}
// → Quen thuộc, dễ hiểu, không cần MediatR.
// → Nhưng: cross-cutting concerns phải handle manual mỗi method.
Master Checklist ✅
□ CQRS level nào? (99% chỉ cần Level 1: tách model)
□ MediatR có giá trị? (complex logic + nhiều use cases + pipeline behaviors)
□ Hay chỉ CRUD thông thường? (→ service/controller đủ)
□ Pipeline behaviors tận dụng? (validation, logging, transaction)
□ Queries dùng Dapper/Projection? (không load full entities cho reads)
□ Commands trả Result, không throw exception cho business errors?
Tư duy master: MediatR là tool, không phải architecture. Nếu bạn dùng MediatR cho project mà mỗi handler chỉ có
await db.SaveChangesAsync(), bạn đang trả overhead cho giá trị = 0.