CQRS và MediatR trong ASP.NET Core: Giải pháp cho những Service ‘nghìn dòng’

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Tại sao phải thay đổi cách viết code truyền thống?

Sáu tháng trước, mình tham gia một dự án Web API quản lý kho vận với quy mô khá lớn. Team có 5 developer và tất cả đều trung thành với kiến trúc 3 lớp (3-tier) truyền thống. Tuy nhiên, chỉ sau 3 tháng, các file Service bắt đầu biến thành những “tòa tháp” chọc trời với hơn 2.000 dòng code.

Mỗi khi cần sửa một logic nhỏ trong phần cập nhật đơn hàng, mình phải “bơi” qua hàng chục hàm đan xen. Việc này cực kỳ rủi ro vì rất dễ gây lỗi dây chuyền sang các tính năng khác. Đó là lúc mình đề xuất áp dụng CQRS (Command Query Responsibility Segregation) kết hợp với thư viện MediatR.

Kết quả mang lại rất ấn tượng. Sau 6 tháng vận hành, thời gian debug của team giảm khoảng 30%, việc viết Unit Test cũng trở nên nhẹ nhàng hơn hẳn. Nếu bạn đang mệt mỏi vì những Controller hay Service quá dày đặc, bài viết này chính là câu trả lời cho bạn.

Thực hành nhanh trong 5 phút (Quick Start)

Thay vì nói suông về lý thuyết, hãy cùng bắt tay vào cài đặt để thấy cách nó vận hành thực tế.

Bước 1: Cài đặt thư viện

Mở Terminal trong project ASP.NET Core và chạy lệnh sau để thêm package MediatR:

dotnet add package MediatR

Bước 2: Tạo Command và Handler

Hãy tưởng tượng bạn cần tạo một sản phẩm mới. Thay vì nhồi nhét vào Service, chúng ta sẽ tạo một class CreateProductCommand riêng biệt:

public record CreateProductCommand(string Name, decimal Price) : IRequest<int>;

public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
    public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        // Giả lập lưu vào Database và trả về ID sản phẩm
        Console.WriteLine($"Đang tạo sản phẩm: {request.Name}");
        return await Task.FromResult(new Random().Next(1, 1000));
    }
}

Bước 3: Đăng ký MediatR

Trong file Program.cs, bạn chỉ cần một dòng code để MediatR tự động tìm thấy các Handler:

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

Bước 4: Gọi từ Controller

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    public ProductsController(IMediator mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductCommand command)
    {
        var id = await _mediator.Send(command);
        return Ok(id);
    }
}

Bản chất của CQRS và MediatR

1. CQRS – Chia để trị

CQRS không phải là một framework thần thánh, nó đơn giản là một tư duy kiến trúc. Thay vì dùng chung một Model cho cả việc đọc và ghi, chúng ta tách chúng làm hai con đường riêng biệt:

  • Commands: Những hành động thay đổi dữ liệu (Thêm, Sửa, Xóa). Chúng thường chỉ trả về ID hoặc trạng thái thành công.
  • Queries: Những hành động lấy dữ liệu (Lấy chi tiết, Danh sách). Các hàm này tuyệt đối không được sửa đổi bất kỳ thứ gì trong Database.

2. MediatR – “Người vận chuyển” trong ứng dụng

MediatR đóng vai trò trung gian (In-process Messaging). Controller giờ đây không cần quan tâm Service nào sẽ xử lý logic. Nó chỉ việc gửi một yêu cầu (Request) đi, MediatR sẽ tự tìm đúng “địa chỉ” (Handler) để thực thi. Cách làm này giúp loại bỏ tình trạng Constructor của Controller bị phình to do phải Inject quá nhiều Service.

Nâng cao: Xử lý Validation và Logging tập trung

Trong các dự án thực tế, mình không bao giờ viết code Validation thủ công bên trong Handler. MediatR cung cấp một tính năng cực mạnh là Pipeline Behavior, hoạt động tương tự như Middleware nhưng dành riêng cho lớp Application.

Ví dụ, để đo hiệu suất của mọi Request, bạn có thể tạo một Logging Behavior như sau:

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        var timer = Stopwatch.StartNew();
        var response = await next();
        timer.Stop();
        
        if (timer.ElapsedMilliseconds > 500)
        {
             Console.WriteLine($"Cảnh báo: Request {typeof(TRequest).Name} chạy chậm: {timer.ElapsedMilliseconds}ms");
        }
        return response;
    }
}

Kinh nghiệm “xương máu” sau 6 tháng triển khai

Áp dụng công nghệ mới luôn đi kèm với những bài học thực tiễn. Dưới đây là những lưu ý để bạn tránh đi vào vết xe đổ:

1. Đừng “dùng dao mổ trâu để giết gà”

Nếu bạn chỉ làm một trang web CRUD đơn giản với vài ba bảng dữ liệu, việc tách Command/Query sẽ làm tốn thời gian vô ích. CQRS chỉ thực sự phát huy sức mạnh khi logic nghiệp vụ bắt đầu phức tạp và cần mở rộng lâu dài.

2. Tổ chức thư mục theo Feature (Vertical Slice)

Thay vì chia theo kiểu Controllers/, Models/ truyền thống, hãy thử chia theo Features. Mọi thứ liên quan đến một chức năng như CreateProduct.cs (gồm cả Command và Handler) nên nằm chung một chỗ. Cách này giúp bạn tìm code nhanh hơn gấp nhiều lần.

3. Tối ưu hiệu suất Query

Một lợi thế lớn của CQRS là bạn có thể dùng Entity Framework cho phía Command để quản lý Transaction, nhưng lại dùng Dapper cho phía Query để tối ưu tốc độ. Trong dự án của mình, việc kết hợp này giúp các API lấy danh sách giảm tới 40% độ trễ (latency).

4. Tuyệt đối tránh Handler gọi Handler

Đây là sai lầm phổ biến nhất. Đừng bao giờ dùng _mediator.Send() bên trong một Handler khác để tái sử dụng logic. Nếu cần dùng chung code, hãy tách phần đó ra một Domain Service hoặc một Helper class độc lập.

Lời kết

CQRS và MediatR không phải là “viên đạn bạc” giải quyết mọi vấn đề, nhưng chúng là công cụ tuyệt vời để giữ cho code luôn ngăn nắp. Việc tách biệt trách nhiệm giúp team tự tin hơn khi nâng cấp hệ thống và giúp nhân sự mới nắm bắt dự án nhanh hơn.

Nếu bạn đang xây dựng một hệ thống Web API có độ phức tạp trung bình trở lên, hãy thử áp dụng ngay. Sự khác biệt về độ sạch của code sau vài tháng vận hành chắc chắn sẽ khiến bạn hài lòng.

Share: