Chapter 05

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

CreateProductCommand
UpdateProductCommand
DeleteProductCommand
GetProductByIdQuery
GetProductsListQuery
ProductCreatedNotification

Verb + Noun + Type suffix. Đọc tên = hiểu mục đích.

DON'T

ProductRequest
ProductModel
HandleProduct
DoStuff
ProductDto (as request)
ProductEvent

Tên mơ hồ, không phân biệt được Command/Query/Notification.

Best Practices

Request Immutability — dùng record +

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.

C#
// 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 — đặt ở Behavior, không phải Handler +

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.

C#
// 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.

DI Lifetime — Transient cho Handlers +

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ột Handler per Request — không dùng chung +

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.

C#
// 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 mỏng — không chứa logic +

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.

C#
// 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);
}
CancellationToken — luôn truyền qua +

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.

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

Error: 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ý.
C#
// 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

Nhầm: Dùng Send() cho INotification hoặc Publish() cho IRequest — sẽ throw exception hoặc compile error.
C#
// 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

Mặc định: Notification handlers chạy tuần tự. Nếu handler thứ 2 throw, handler thứ 3, 4... sẽ không chạy. Đây là behavior mặc định rất nguy hiểm cho side effects.
C#
// 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

Không phải mọi thứ đều cần MediatR. Nếu ứng dụng nhỏ, CRUD đơn giản, không có cross-cutting concerns phức tạp — inject service trực tiếp vẫn tốt hơn. MediatR thêm indirection, và indirection có chi phí.

Dấu hiệu nên dùng MediatR:

Dấu hiệu không nên dùng:

5 Handler gọi Send() lồng nhau

Anti-pattern: Handler A gọi 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.
C#
// 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

C#
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

C#
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);
    }
}
Unit test handler không cần mock MediatR. Handler là một class bình thường — tạo instance, inject dependencies, gọi 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ỏ:

Overhead của MediatR +

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.

Khi nào cần cẩn thận? +
  • 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.