Best Practices
& Lưu ý
Naming conventions, pitfalls thường gặp, testing, performance
Naming Conventions
Đặt tên rõ ràng giúp team hiểu ngay mục đích của request mà không cần mở file:
DO
CreateProductCommandUpdateProductCommandDeleteProductCommandGetProductByIdQueryGetProductsListQueryProductCreatedNotification
Verb + Noun + Type suffix. Đọc tên = hiểu mục đích.
DON'T
ProductRequestProductModelHandleProductDoStuffProductDto (as request)ProductEvent
Tên mơ hồ, không phân biệt được Command/Query/Notification.
Best Practices
Luôn dùng record cho requests thay vì class. Records là immutable by default,
hỗ trợ value equality, và syntax ngắn gọn với primary constructor.
// DO: dùng record public record CreateProductCommand( string Name, decimal Price ) : ICommand<int>; // DON'T: dùng class với properties public class CreateProductCommand : ICommand<int> { public string Name { get; set; } // Mutable! public decimal Price { get; set; } }
Validation nên nằm trong ValidationBehavior (cross-cutting), không phải trong từng handler.
Handler chỉ chứa business logic chính. Điều này đảm bảo mọi request đều được validate
trước khi handler chạy.
// DO: Validator riêng + ValidationBehavior public class CreateProductValidator : AbstractValidator<CreateProductCommand> { ... } // DON'T: validate trong handler public class CreateProductHandler : ... { public async Task<int> Handle(...) { if (string.IsNullOrEmpty(request.Name)) // Mixing concerns! throw new ...; } }
Ngoại lệ: Business rule validation (phụ thuộc database, ví dụ "tên sản phẩm đã tồn tại") nên nằm trong handler vì cần truy cập database.
MediatR đăng ký handlers và behaviors mặc định là Transient. Đây là lifetime đúng — đừng đổi sang Singleton hay Scoped trừ khi có lý do rõ ràng.
Tại sao Transient?
- Handlers thường inject
DbContext(Scoped) — Singleton handler + Scoped DbContext = captive dependency bug - Transient đảm bảo mỗi request có handler instance mới, không shared state
- Chi phí tạo instance handler rất thấp (chỉ là object allocation)
Mỗi request phải có đúng một handler riêng. Đừng tạo một "mega handler" xử lý nhiều loại request. Nếu thấy handler quá lớn, đó là dấu hiệu cần tách thành nhiều requests.
// DO: một handler per request public class CreateProductHandler : IRequestHandler<CreateProductCommand, int> { ... } public class UpdateProductHandler : IRequestHandler<UpdateProductCommand> { ... } // DON'T: handler implement nhiều interfaces public class ProductMegaHandler : IRequestHandler<CreateProductCommand, int>, IRequestHandler<UpdateProductCommand>, IRequestHandler<DeleteProductCommand> { ... } // Giống lại Service class!
Controller chỉ nên: (1) nhận HTTP input, (2) map sang MediatR request, (3) gọi Send(), (4)
trả HTTP response. Không if/else, không try/catch (trừ global exception handler), không gọi service trực
tiếp.
// DO: controller siêu mỏng [HttpPost] public async Task<IActionResult> Create(CreateProductCommand command) => Ok(await _sender.Send(command)); // DON'T: logic trong controller [HttpPost] public async Task<IActionResult> Create(CreateProductDto dto) { if (!ModelState.IsValid) return BadRequest(); var product = _mapper.Map(dto); await _service.CreateAsync(product); await _email.SendNotification(product); return Ok(product.Id); }
Luôn nhận và truyền CancellationToken trong handlers. Khi user đóng trình duyệt hoặc request
bị timeout, token sẽ được cancel. Nếu handler bỏ qua token, nó vẫn chạy tới khi xong — lãng phí tài
nguyên server.
// DO: truyền CancellationToken cho mọi async call var products = await _db.Products .ToListAsync(ct); // ← truyền ct // DON'T: bỏ qua CancellationToken var products = await _db.Products .ToListAsync(); // ← không cancel được!
Common Pitfalls
1 Quên đăng ký Assembly
System.InvalidOperationException: Handler was not found for request of type...Nguyên nhân:
AddMediatR() chưa scan assembly chứa handler. Fix: đảm bảo truyền đúng assembly khi đăng
ký.
// Handler nằm ở Application.dll nhưng chỉ scan WebApi.dll builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly) ); // ↑ KHÔNG tìm thấy handlers ở Application.dll! // Fix: scan cả Application assembly builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( typeof(Program).Assembly, typeof(CreateProductCommand).Assembly // Application.dll ) );
2 Send() vs Publish() nhầm lẫn
Send() cho INotification hoặc Publish() cho
IRequest — sẽ throw exception hoặc compile error.
// DO await _sender.Send(new CreateProductCommand(...)); // IRequest → Send() await _publisher.Publish(new ProductCreatedNotification(...)); // INotification → Publish() // DON'T await _sender.Send(new ProductCreatedNotification(...)); // WRONG! await _publisher.Publish(new CreateProductCommand(...)); // WRONG!
3 Notification Handler throw Exception
// DO: bắt exception trong notification handler public async Task Handle( ProductCreatedNotification notification, CancellationToken ct) { try { await _email.SendAsync(...); } catch (Exception ex) { _logger.LogError(ex, "Failed to send email for product {Id}", notification.ProductId); // KHÔNG re-throw — cho phép handlers tiếp theo chạy } }
4 Over-engineering
Dấu hiệu nên dùng MediatR:
- Ứng dụng có >10 controllers, logic phức tạp
- Cần cross-cutting: validation, logging, caching, authorization ở tầng application
- Team muốn áp dụng CQRS hoặc Clean Architecture
- Nhiều team cùng phát triển, cần giảm coupling
Dấu hiệu không nên dùng:
- CRUD app đơn giản, ít logic
- Team nhỏ, dự án nhỏ, cần shipping nhanh
- Không có cross-cutting concerns
- Chỉ dùng để "cho đẹp" mà không có lợi ích thực tế
5 Handler gọi Send() lồng nhau
Send() để trigger Handler B. Điều này tạo dependency ẩn
giữa handlers, khó debug, khó trace. Nếu cần gọi logic từ handler khác, extract thành shared service.
// DON'T: handler gọi Send() lồng nhau public class CreateOrderHandler : ... { private readonly ISender _sender; public async Task Handle(...) { // Gọi handler khác — anti-pattern! var product = await _sender.Send( new GetProductByIdQuery(request.ProductId)); ... } } // DO: inject shared service trực tiếp public class CreateOrderHandler : ... { private readonly AppDbContext _db; // Truy cập trực tiếp public async Task Handle(...) { var product = await _db.Products .FindAsync(request.ProductId); ... } }
Testing Patterns
Unit Test Handler
Test handler trực tiếp — không cần MediatR. Tạo instance, mock dependencies, gọi Handle().
Unit Test Behavior
Test behavior riêng — mock RequestHandlerDelegate<T> làm next().
Test Handler
public class CreateProductHandlerTests { [Fact] public async Task Handle_ValidCommand_ReturnsProductId() { // Arrange var db = CreateInMemoryDbContext(); var handler = new CreateProductHandler(db); var command = new CreateProductCommand( "Laptop", 999.99m, "Electronics"); // Act var id = await handler.Handle(command, CancellationToken.None); // Assert Assert.True(id > 0); var product = await db.Products.FindAsync(id); Assert.Equal("Laptop", product!.Name); Assert.Equal(999.99m, product.Price); } [Fact] public async Task Handle_NotFound_ThrowsNotFoundException() { var db = CreateInMemoryDbContext(); var handler = new GetProductByIdHandler(db); await Assert.ThrowsAsync<NotFoundException>( () => handler.Handle( new GetProductByIdQuery(999), CancellationToken.None)); } }
Test Behavior
public class ValidationBehaviorTests { [Fact] public async Task Handle_InvalidRequest_ThrowsValidationException() { // Arrange var validators = new List<IValidator<CreateProductCommand>> { new CreateProductValidator() }; var behavior = new ValidationBehavior< CreateProductCommand, int>(validators); var invalidCommand = new CreateProductCommand( "", -1, ""); // Invalid! RequestHandlerDelegate<int> next = () => Task.FromResult(1); // Mock next // Act & Assert await Assert.ThrowsAsync<ValidationException>( () => behavior.Handle( invalidCommand, next, CancellationToken.None)); } [Fact] public async Task Handle_NoValidators_CallsNext() { var behavior = new ValidationBehavior< CreateProductCommand, int>( Enumerable.Empty<IValidator<CreateProductCommand>>()); var nextCalled = false; RequestHandlerDelegate<int> next = () => { nextCalled = true; return Task.FromResult(42); }; var result = await behavior.Handle( new CreateProductCommand("Test", 10, "Cat"), next, CancellationToken.None); Assert.True(nextCalled); Assert.Equal(42, result); } }
Handle(). Đây chính là lợi ích lớn nhất của MediatR: handler dễ test vì
không phụ thuộc vào framework.
Performance
MediatR thêm một lớp indirection, nhưng overhead rất nhỏ:
MediatR resolve handler từ DI container rồi gọi Handle(). Overhead chính là DI resolution
— thường dưới 1 microsecond. So với thời gian database query (1-100ms), HTTP call
(10-1000ms), overhead này không đáng kể.
Nếu ứng dụng chậm, nguyên nhân hầu như không bao giờ là MediatR. Hãy kiểm tra database queries, N+1 problems, missing indexes trước.
- Quá nhiều behaviors: 10+ behaviors cho mọi request sẽ tạo call chain dài. Chỉ giữ behaviors thực sự cần thiết.
- Behaviors nặng: Logging behavior serialize toàn bộ request object sẽ chậm với request lớn. Dùng structured logging thay vì serialize JSON.
- Notification handlers nhiều: 20 handlers cho một notification sẽ chạy tuần tự. Cân nhắc giới hạn hoặc chuyển sang background processing.