Requests &
Handlers
Tất cả các loại message trong MediatR — Request, Notification, Stream
Ba loại Message
MediatR cung cấp ba mô hình giao tiếp, mỗi loại phù hợp với một tình huống khác nhau:
Request / Response
Một request → đúng một handler. Có thể trả về response hoặc không (void). Dùng cho mọi use case chính.
Notification
Một notification → 0 hoặc nhiều handlers. Pub/Sub pattern. Dùng cho side effects, events.
Stream Request
Request trả về IAsyncEnumerable. Dùng khi response là luồng dữ liệu lớn, trả dần từng phần.
Request / Response
Đây là loại message phổ biến nhất. Mỗi request phải có đúng một handler. Nếu không tìm thấy handler, MediatR sẽ throw exception.
Request có Response
Implement IRequest<TResponse> khi cần trả về kết quả:
// Request: lấy danh sách sản phẩm theo category public record GetProductsByCategoryQuery( string Category, int Page = 1, int PageSize = 20 ) : IRequest<PagedResult<ProductDto>>; // Handler public class GetProductsByCategoryHandler : IRequestHandler<GetProductsByCategoryQuery, PagedResult<ProductDto>> { private readonly AppDbContext _db; public GetProductsByCategoryHandler(AppDbContext db) => _db = db; public async Task<PagedResult<ProductDto>> Handle( GetProductsByCategoryQuery request, CancellationToken ct) { var query = _db.Products .Where(p => p.Category == request.Category) .OrderBy(p => p.Name); var total = await query.CountAsync(ct); var items = await query .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .Select(p => new ProductDto(p.Id, p.Name, p.Price)) .ToListAsync(ct); return new PagedResult<ProductDto>(items, total, request.Page, request.PageSize); } }
Request không có Response (void)
Implement IRequest (không generic) cho các hành động không cần trả kết quả:
// Request: xóa sản phẩm public record DeleteProductCommand(int Id) : IRequest; // Handler: trả về Unit (tương đương void) public class DeleteProductHandler : IRequestHandler<DeleteProductCommand> { private readonly AppDbContext _db; public DeleteProductHandler(AppDbContext db) => _db = db; public async Task Handle( DeleteProductCommand request, CancellationToken ct) { var product = await _db.Products.FindAsync( new object[] { request.Id }, ct) ?? throw new NotFoundException("Product", request.Id); _db.Products.Remove(product); await _db.SaveChangesAsync(ct); } }
IRequestHandler<TRequest> (void) trả về Task thay vì
Task<Unit>. Bạn không cần return Unit.Value; nữa.
Notification (Pub/Sub)
Notification phát sự kiện đến tất cả handlers đã đăng ký. Không có response. Hữu ích cho side effects như gửi email, ghi log, cập nhật cache sau khi một hành động hoàn tất.
// Notification: sản phẩm vừa được tạo public record ProductCreatedNotification( int ProductId, string ProductName, decimal Price ) : INotification; // Handler 1: gửi email cho admin public class SendAdminEmailHandler : INotificationHandler<ProductCreatedNotification> { private readonly IEmailService _email; public SendAdminEmailHandler(IEmailService email) => _email = email; public async Task Handle( ProductCreatedNotification notification, CancellationToken ct) { await _email.SendAsync( "admin@company.com", $"New product: {notification.ProductName}", $"Price: {notification.Price:C}", ct); } } // Handler 2: ghi audit log public class AuditLogHandler : INotificationHandler<ProductCreatedNotification> { private readonly IAuditService _audit; public AuditLogHandler(IAuditService audit) => _audit = audit; public async Task Handle( ProductCreatedNotification notification, CancellationToken ct) { await _audit.LogAsync( "ProductCreated", $"Id={notification.ProductId}", ct); } }
Publish từ Handler
// Trong CreateProductHandler, sau khi tạo xong — publish notification public class CreateProductHandler : IRequestHandler<CreateProductCommand, int> { private readonly AppDbContext _db; private readonly IPublisher _publisher; public CreateProductHandler(AppDbContext db, IPublisher publisher) { _db = db; _publisher = publisher; } public async Task<int> Handle( CreateProductCommand request, CancellationToken ct) { var product = new Product(request.Name, request.Price); _db.Products.Add(product); await _db.SaveChangesAsync(ct); // Publish notification — tất cả handlers sẽ chạy await _publisher.Publish( new ProductCreatedNotification(product.Id, product.Name, product.Price), ct); return product.Id; } }
Stream Request
Từ MediatR v10+, bạn có thể stream response dưới dạng IAsyncEnumerable<T>. Hữu ích khi dữ liệu
lớn và muốn trả về từng phần thay vì đợi toàn bộ.
// Stream Request public record StreamProductsQuery( string Category ) : IStreamRequest<ProductDto>; // Stream Handler public class StreamProductsHandler : IStreamRequestHandler<StreamProductsQuery, ProductDto> { private readonly AppDbContext _db; public StreamProductsHandler(AppDbContext db) => _db = db; public async IAsyncEnumerable<ProductDto> Handle( StreamProductsQuery request, [EnumeratorCancellation] CancellationToken ct) { await foreach (var p in _db.Products .Where(p => p.Category == request.Category) .AsAsyncEnumerable() .WithCancellation(ct)) { yield return new ProductDto(p.Id, p.Name, p.Price); } } } // Sử dụng trong Controller [HttpGet("stream")] public async IAsyncEnumerable<ProductDto> StreamProducts( [FromQuery] string category, [EnumeratorCancellation] CancellationToken ct) { await foreach (var item in _sender.CreateStream( new StreamProductsQuery(category), ct)) { yield return item; } }
Controller Integration
MediatR cung cấp ba interface để inject vào controller. Chọn interface nhỏ nhất phù hợp với nhu cầu:
| Interface | Methods | Khi nào dùng |
|---|---|---|
ISender |
Send() |
Chỉ cần gửi request/response |
IPublisher |
Publish() |
Chỉ cần phát notification |
IMediator |
Send() + Publish() + CreateStream() |
Cần cả Send lẫn Publish |
[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ISender _sender; public ProductsController(ISender sender) => _sender = sender; [HttpGet] public async Task<IActionResult> GetByCategory( [FromQuery] string category, [FromQuery] int page = 1) { var result = await _sender.Send( new GetProductsByCategoryQuery(category, page)); return Ok(result); } [HttpPost] public async Task<IActionResult> Create( [FromBody] CreateProductCommand command) { var id = await _sender.Send(command); return CreatedAtAction(nameof(GetById), new { id }, null); } [HttpDelete("{id:int}")] public async Task<IActionResult> Delete(int id) { await _sender.Send(new DeleteProductCommand(id)); return NoContent(); } }
Khi nào dùng gì?
| Tình huống | Loại Message | Lý do |
|---|---|---|
| Lấy dữ liệu (query) | IRequest<T> |
Cần response, đúng một handler xử lý |
| Tạo/Sửa/Xóa (command) | IRequest hoặc IRequest<T> |
Thao tác chính, có thể trả về ID hoặc void |
| Side effects (email, log, cache) | INotification |
Nhiều handlers cùng phản ứng, không cần response |
| Export CSV / Stream lớn | IStreamRequest<T> |
Dữ liệu lớn, trả dần từng phần |
| Domain events (DDD) | INotification |
Pub/Sub, nhiều bounded contexts phản ứng |