
Triển khai CQRS với thư viện MediatR
Table Of Content
Giới Thiệu Về CQRS
CQRS (Command Query Responsibility Segregation) là một mô hình thiết kế giúp tách biệt hai luồng ghi (Command) và đọc (Query) trong hệ thống. Ví dụ, trong một hệ thống e-commerce, khi người dùng đặt hàng, thông tin đơn hàng sẽ được xử lý bởi luồng ghi (Command), sau đó, luồng đọc (Query) sẽ lấy dữ liệu từ một nguồn tối ưu hóa riêng để hiển thị lịch sử đơn hàng nhanh chóng. Điều này giúp hệ thống hoạt động hiệu quả hơn mà không làm ảnh hưởng đến hiệu suất xử lý giao dịch.
Lợi ích khi dùng CQRS:
- Tối ưu hiệu suất: Các thao tác ghi và đọc được thực hiện trên hai hệ thống tách biệt, giúp tăng tốc độ xử lý và giảm tải.
- Hỗ trợ Event Sourcing: Lưu trữ toàn bộ thay đổi của dữ liệu, giúp dễ dàng kiểm soát lịch sử và rollback khi cần.
- Tăng khả năng mở rộng: Hệ thống có thể phát triển theo chiều ngang, với các database hoặc cache riêng biệt cho ghi và đọc.
- Tăng tính bảo mật: Tách rời quyền truy cập ghi và đọc, hạn chế tác động ngoài ý muốn đến dữ liệu quan trọng.
- Cải thiện bảo trì và nâng cấp: Mỗi phần có thể được phát triển độc lập mà không ảnh hưởng đến phần còn lại.
MediatR Trong CQRS
MediatR là một thư viện giúp giảm phụ thuộc (decoupling) giữa Controller và Service bằng cách trung gian hóa giao tiếp giữa Command, Query và Handler. Khi một lệnh (Command) được gửi từ Controller, MediatR sẽ tự động chuyển tiếp nó đến Handler thích hợp mà không cần Controller phải biết chính xác Handler nào sẽ xử lý lệnh đó. Điều này giúp duy trì nguyên tắc Single Responsibility và tăng khả năng mở rộng của hệ thống.
Dưới đây là sơ đồ mô tả luồng xử lý trong MediatR:
- Controller gửi một
Command
hoặcQuery
thông quaIMediator
. - MediatR xác định và chuyển tiếp yêu cầu đến Handler phù hợp.
- Handler thực hiện logic nghiệp vụ và trả kết quả.
- MediatR nhận kết quả từ Handler và chuyển lại cho Controller.
Ví dụ:
- Khi gọi API
/api/orders
, Controller gửiCreateOrderCommand
đến MediatR. - MediatR định tuyến command đến
CreateOrderHandler
, nơi logic tạo đơn hàng được xử lý. - Sau khi xử lý xong, kết quả được gửi lại Controller để trả về response cho client.
Lợi ích của MediatR:
- Loại bỏ dependency injection phức tạp: Không cần inject nhiều service vào controller.
- Dễ test: Handler tách biệt giúp unit test dễ dàng hơn.
- Dễ dàng mở rộng: Có thể dễ dàng thêm Handler mới mà không ảnh hưởng đến các phần khác.
- Cải thiện khả năng bảo trì: Code rõ ràng, dễ đọc, dễ mở rộng mà không bị rối.
Cài Đặt CQRS Trong .NET Core
Cài Đặt MediatR
Thêm MediatR và MediatR.Extensions.Microsoft.DependencyInjection:
dotnet add package MediatR
Cài đặt tại Program.cs
:
using MediatR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMediatR(typeof(Program).Assembly);
Kiểm Tra MediatR Bằng Unit Test
Để kiểm tra xem MediatR đã hoạt động đúng hay chưa, ta có thể viết một unit test đơn giản sử dụng Moq
và xUnit
:
public class MediatRTests
{
private readonly IMediator _mediator;
private readonly Mock<IRequestHandler<CreateOrderCommand, Guid>> _handlerMock;
public MediatRTests()
{
_handlerMock = new Mock<IRequestHandler<CreateOrderCommand, Guid>>();
var services = new ServiceCollection();
services.AddMediatR(typeof(CreateOrderHandler).Assembly);
services.AddTransient(_ => _handlerMock.Object);
var provider = services.BuildServiceProvider();
_mediator = provider.GetRequiredService<IMediator>();
}
[Fact]
public async Task CreateOrderCommand_Should_Invoke_Handler()
{
// Arrange
var command = new CreateOrderCommand("Test Product", 1);
_handlerMock.Setup(h => h.Handle(It.IsAny<CreateOrderCommand>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Guid.NewGuid());
// Act
var result = await _mediator.Send(command);
// Assert
_handlerMock.Verify(h => h.Handle(It.Is<CreateOrderCommand>(c => c.ProductName == "Test Product"), It.IsAny<CancellationToken>()), Times.Once);
Assert.NotEqual(Guid.Empty, result);
}
}
Test này đảm bảo rằng CreateOrderCommand
được xử lý chính xác bởi Handler thông qua MediatR.
Tạo Command (Để Ghi Dữ Liệu)
Command chịu trách nhiệm xử lý yêu cầu thay đổi trạng thái hệ thống, như tạo mới, cập nhật dữ liệu.
MediatR xử lý command thông qua lớp Dispatcher, hoạt động như một trung gian để định tuyến command đến đúng handler. Khi một command được gửi thông qua IMediator
, nó sẽ:
- Xác định loại command và tìm kiếm handler phù hợp.
- Gửi command đến handler tương ứng để thực hiện logic nghiệp vụ.
- Nhận kết quả từ handler và chuyển về cho caller.
Điều này giúp loại bỏ dependency injection trực tiếp vào Controller và đảm bảo rằng mọi command luôn được xử lý đúng chỗ.
public record CreateOrderCommand(string ProductName, int Quantity) : IRequest<Guid>;
Tạo Handler xử lý Command:
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _repository;
public CreateOrderHandler(IOrderRepository repository)
{
_repository = repository;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order { Id = Guid.NewGuid(), ProductName = request.ProductName, Quantity = request.Quantity };
await _repository.AddAsync(order);
return order.Id;
}
}
Tạo Query (Để Đọc Dữ Liệu)
Query chịu trách nhiệm lấy dữ liệu mà không làm thay đổi trạng thái của hệ thống.
public record GetOrderByIdQuery(Guid OrderId) : IRequest<Order>;
Tạo Handler xử lý Query:
public class GetOrderByIdHandler : IRequestHandler<GetOrderByIdQuery, Order>
{
private readonly IOrderRepository _repository;
public GetOrderByIdHandler(IOrderRepository repository)
{
_repository = repository;
}
public async Task<Order> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.OrderId);
}
}
Triển Khai API Controller
Controller sử dụng MediatR để dispatch Command & Query mà không cần gọi trực tiếp repository.
[ApiController]
[Route("api/orders")]
public class OrderController : ControllerBase
{
private readonly IMediator _mediator;
public OrderController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return Ok(orderId);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(Guid id)
{
var order = await _mediator.Send(new GetOrderByIdQuery(id));
return order is null ? NotFound() : Ok(order);
}
}
Kết Luận
✅ CQRS tách biệt logic ghi và đọc, giúp tối ưu hiệu suất và giảm tải database.
✅ MediatR giúp dispatch Command & Query dễ dàng, loại bỏ sự phụ thuộc giữa Controller và Repository. ✅ Hệ thống dễ test, bảo trì và mở rộng. CQRS và MediatR giúp ích đặc biệt trong các hệ thống có yêu cầu cao về mở rộng và hiệu suất, chẳng hạn như:
- Ứng dụng thương mại điện tử: Tách biệt xử lý đơn hàng (ghi) và hiển thị danh sách sản phẩm (đọc) giúp giảm tải hệ thống.
- Hệ thống tài chính: Ghi nhận giao dịch và truy vấn lịch sử tài khoản có thể được tối ưu hóa bằng hai hệ thống riêng biệt.
- Ứng dụng IoT: Dữ liệu cảm biến có thể được xử lý riêng lẻ từ các truy vấn phân tích tổng hợp để đảm bảo hiệu suất.
- Hệ thống quản lý khách hàng (CRM): Việc cập nhật thông tin khách hàng diễn ra tách biệt với việc truy vấn báo cáo, giúp tối ưu hóa trải nghiệm người dùng., đặc biệt phù hợp với Microservices, Banking, eCommerce.