Chapter 02

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ả:

C#
// 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ả:

C#
// 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);
    }
}
Từ MediatR v12+, 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.

Publish
notification
Handler 1
gửi email
Handler 2
ghi audit log
Handler 3
invalidate cache
C#
// 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

C#
// 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;
    }
}
Lưu ý: Notification handlers mặc định chạy tuần tự (sequential). Nếu một handler throw exception, các handlers còn lại sẽ không chạy. Đừng để notification handler throw — hãy bắt exception và log thay vì để lan truyền.

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ộ.

C#
// 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
C#
[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();
    }
}
Controller chỉ làm hai việc: (1) nhận input từ HTTP request, (2) tạo MediatR request và gửi đi. Không nên chứa business logic. Giữ controller mỏng nhất có thể.

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