Files
pos-system/.agent/skills/cqrs-mediatr/SKILL.md
Ho Ngoc Hai df007cafde chore: Update compatibility for skills to .NET 10+
- Revised compatibility specifications in multiple skill documentation files to reflect support for .NET 10+, ensuring alignment with the latest development standards.
- Updated files include API design, error handling patterns, project rules, repository pattern, security, and testing patterns.
- This change enhances the relevance and usability of the skills for current and future development projects.
2026-01-14 11:37:28 +07:00

14 KiB

name, description, compatibility, metadata
name description compatibility metadata
cqrs-mediatr CQRS pattern với MediatR. Use for Commands, Queries, Handlers, Pipeline Behaviors, và Idempotency. .NET 10+, MediatR 12+, FluentValidation
author version
Velik Ho 1.0

CQRS & MediatR Patterns / Mẫu CQRS & MediatR

CQRS pattern với MediatR cho GoodGo microservices.

When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:

  • Separating read/write operations / Tách biệt operations đọc/ghi
  • Creating Commands and Queries / Tạo Commands và Queries
  • Implementing MediatR handlers / Triển khai MediatR handlers
  • Adding cross-cutting concerns via Behaviors / Thêm behaviors xuyên suốt
  • Ensuring idempotency / Đảm bảo tính idempotent

Core Concepts / Khái Niệm Cốt Lõi

CQRS Overview / Tổng Quan CQRS

┌─────────────────────────────────────────────────────────────┐
│                        API Controller                        │
└─────────────────────┬───────────────────────────┬───────────┘
                      │                           │
              ┌───────▼───────┐           ┌───────▼───────┐
              │   Commands    │           │    Queries    │
              │ (Write/Ghi)   │           │  (Read/Đọc)   │
              └───────┬───────┘           └───────┬───────┘
                      │                           │
              ┌───────▼───────┐           ┌───────▼───────┐
              │   MediatR     │           │   MediatR     │
              │   Handler     │           │   Handler     │
              └───────┬───────┘           └───────┬───────┘
                      │                           │
              ┌───────▼───────┐           ┌───────▼───────┐
              │ Domain Model  │           │    Dapper     │
              │   EF Core     │           │   Raw SQL     │
              └───────┬───────┘           └───────┬───────┘
                      │                           │
              └───────────────┬───────────────────┘
                              │
                     ┌────────▼────────┐
                     │    Database     │
                     └─────────────────┘

Command vs Query / Lệnh vs Truy Vấn

Aspect Command Query
Purpose Modify state Read data
Returns Result/void Data/DTO
Model Full Domain Model Lightweight DTO
ORM EF Core Dapper/Raw SQL
Validation Full business rules Minimal

MediatR Flow / Luồng MediatR

Request → Pipeline Behaviors → Handler → Response
          ├── LoggingBehavior
          ├── ValidationBehavior
          └── TransactionBehavior

Key Patterns / Mẫu Chính

Command Definition / Định Nghĩa Command

/// <summary>
/// EN: Command to create a new order.
/// VI: Command tạo order mới.
/// </summary>
public record CreateOrderCommand(
    string UserId,
    Address ShippingAddress,
    List<OrderItemDto> Items) : IRequest<OrderResult>;

/// <summary>
/// EN: Command result.
/// VI: Kết quả command.
/// </summary>
public record OrderResult(Guid OrderId);

Query Definition / Định Nghĩa Query

/// <summary>
/// EN: Query to get user orders.
/// VI: Query lấy orders của user.
/// </summary>
public record GetUserOrdersQuery(
    string UserId,
    int Skip = 0,
    int Take = 20) : IRequest<PagedResult<OrderSummaryDto>>;

/// <summary>
/// EN: Lightweight DTO for query results.
/// VI: DTO nhẹ cho kết quả query.
/// </summary>
public record OrderSummaryDto(
    Guid Id,
    string Status,
    decimal TotalAmount,
    DateTime CreatedAt,
    int ItemCount);

Command Handler / Handler Command

/// <summary>
/// EN: Handler for CreateOrderCommand.
/// VI: Handler cho CreateOrderCommand.
/// </summary>
public class CreateOrderCommandHandler 
    : IRequestHandler<CreateOrderCommand, OrderResult>
{
    private readonly IOrderRepository _orderRepository;
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    public CreateOrderCommandHandler(
        IOrderRepository orderRepository,
        ILogger<CreateOrderCommandHandler> logger)
    {
        _orderRepository = orderRepository;
        _logger = logger;
    }

    public async Task<OrderResult> Handle(
        CreateOrderCommand request,
        CancellationToken ct)
    {
        // EN: Create order through domain model
        // VI: Tạo order qua domain model
        var order = new Order(request.UserId, request.ShippingAddress);

        foreach (var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
        }

        await _orderRepository.AddAsync(order, ct);
        await _orderRepository.UnitOfWork.SaveChangesAsync(ct);

        _logger.LogInformation(
            "EN: Order created / VI: Order đã tạo: {OrderId}", 
            order.Id);

        return new OrderResult(order.Id);
    }
}

Query Handler with Dapper / Handler Query với Dapper

/// <summary>
/// EN: Query handler using Dapper for optimized reads.
/// VI: Query handler dùng Dapper cho đọc tối ưu.
/// </summary>
public class GetUserOrdersQueryHandler 
    : IRequestHandler<GetUserOrdersQuery, PagedResult<OrderSummaryDto>>
{
    private readonly IDbConnection _connection;

    public GetUserOrdersQueryHandler(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<PagedResult<OrderSummaryDto>> Handle(
        GetUserOrdersQuery request,
        CancellationToken ct)
    {
        const string countSql = "SELECT COUNT(*) FROM Orders WHERE UserId = @UserId";
        var total = await _connection.ExecuteScalarAsync<int>(countSql, new { request.UserId });

        const string sql = @"
            SELECT o.Id, o.Status, o.TotalAmount, o.CreatedAt,
                   COUNT(oi.Id) as ItemCount
            FROM Orders o
            LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
            WHERE o.UserId = @UserId
            GROUP BY o.Id, o.Status, o.TotalAmount, o.CreatedAt
            ORDER BY o.CreatedAt DESC
            OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY";

        var orders = await _connection.QueryAsync<OrderSummaryDto>(sql, new
        {
            request.UserId,
            request.Skip,
            request.Take
        });

        return new PagedResult<OrderSummaryDto>(orders.ToList(), total);
    }
}

Validation Behavior / Behavior Validation

/// <summary>
/// EN: Pipeline behavior for FluentValidation.
/// VI: Pipeline behavior cho FluentValidation.
/// </summary>
public class ValidationBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);
        var failures = (await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, ct))))
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
            throw new ValidationException(failures);

        return await next();
    }
}

Logging Behavior / Behavior Logging

/// <summary>
/// EN: Pipeline behavior for logging.
/// VI: Pipeline behavior cho logging.
/// </summary>
public class LoggingBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        var requestName = typeof(TRequest).Name;

        _logger.LogInformation(
            "EN: Handling {RequestName} / VI: Xử lý {RequestName}",
            requestName);

        var stopwatch = Stopwatch.StartNew();
        var response = await next();
        stopwatch.Stop();

        _logger.LogInformation(
            "EN: Handled {RequestName} in {ElapsedMs}ms / VI: Đã xử lý {RequestName} trong {ElapsedMs}ms",
            requestName, stopwatch.ElapsedMilliseconds);

        return response;
    }
}

Controller with MediatR / Controller với MediatR

/// <summary>
/// EN: Slim controller using MediatR.
/// VI: Controller gọn nhẹ với MediatR.
/// </summary>
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;

    public OrdersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<ActionResult<ApiResponse<OrderResult>>> CreateOrder(
        CreateOrderRequest request,
        CancellationToken ct)
    {
        var command = new CreateOrderCommand(
            GetUserId(),
            request.ShippingAddress,
            request.Items);

        var result = await _mediator.Send(command, ct);

        return CreatedAtAction(
            nameof(GetOrder),
            new { orderId = result.OrderId },
            ApiResponse<OrderResult>.Ok(result));
    }

    [HttpGet]
    public async Task<ActionResult<ApiResponse<PagedResult<OrderSummaryDto>>>> GetOrders(
        [FromQuery] int skip = 0,
        [FromQuery] int take = 20,
        CancellationToken ct = default)
    {
        var query = new GetUserOrdersQuery(GetUserId(), skip, take);
        var result = await _mediator.Send(query, ct);
        return Ok(ApiResponse<PagedResult<OrderSummaryDto>>.Ok(result));
    }

    private string GetUserId() =>
        User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new UnauthorizedAccessException();
}

Common Mistakes / Lỗi Thường Gặp

1. Using Domain Model for Queries

// ❌ BAD: Using EF Core for simple reads
public async Task<IEnumerable<Order>> Handle(GetOrdersQuery query, CancellationToken ct)
{
    return await _context.Orders
        .Include(o => o.OrderItems)
        .Where(o => o.UserId == query.UserId)
        .ToListAsync(ct);
}

// ✅ GOOD: Using Dapper for optimized reads
public async Task<IEnumerable<OrderDto>> Handle(GetOrdersQuery query, CancellationToken ct)
{
    const string sql = "SELECT Id, Status, TotalAmount FROM Orders WHERE UserId = @UserId";
    return await _connection.QueryAsync<OrderDto>(sql, new { query.UserId });
}

2. Missing Pipeline Behaviors

// ❌ BAD: Validation in handler
public async Task<OrderResult> Handle(CreateOrderCommand request, CancellationToken ct)
{
    if (string.IsNullOrEmpty(request.UserId))
        throw new ValidationException("UserId required");
    // ...
}

// ✅ GOOD: Validation via Behavior
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});

3. Fat Controllers

// ❌ BAD: Logic in controller
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
    var order = new Order(request.UserId, request.Address);
    foreach (var item in request.Items)
        order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
    await _repository.AddAsync(order);
    await _repository.UnitOfWork.SaveChangesAsync();
    return Ok(order.Id);
}

// ✅ GOOD: Delegate to MediatR
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
    var command = request.ToCommand(GetUserId());
    var result = await _mediator.Send(command);
    return CreatedAtAction(nameof(GetOrder), new { orderId = result.OrderId }, result);
}

Quick Reference / Tham Chiếu Nhanh

MediatR Request Types

Interface Purpose Example
IRequest<T> Request with response Commands, Queries
IRequest Request without response Fire-and-forget
INotification Event notification Domain events

Pipeline Behavior Order

// EN: Registration order = execution order
// VI: Thứ tự đăng ký = thứ tự thực thi
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));      // 1st
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));   // 2nd
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));  // 3rd

DI Registration

// EN: Register MediatR with behaviors
// VI: Đăng ký MediatR với behaviors
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});

// EN: Register FluentValidation
// VI: Đăng ký FluentValidation
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

Resources / Tài Nguyên