# CQRS-MediatR - Detailed Reference

Detailed code examples for CQRS pattern với MediatR trong ASP.NET Core.

## Table of Contents

1. [Project Setup](#project-setup)
2. [Command Patterns](#command-patterns)
3. [Query Patterns](#query-patterns)
4. [Pipeline Behaviors](#pipeline-behaviors)
5. [Idempotency](#idempotency)
6. [Domain Events](#domain-events)
7. [Transaction Management](#transaction-management)

---

## Project Setup

### Package Dependencies

```xml
<ItemGroup>
  <PackageReference Include="MediatR" Version="12.2.0" />
  <PackageReference Include="FluentValidation" Version="11.9.0" />
  <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
  <PackageReference Include="Dapper" Version="2.1.24" />
</ItemGroup>
```

### DI Registration

```csharp
/// <summary>
/// EN: Configure MediatR with all behaviors.
/// VI: Cấu hình MediatR với tất cả behaviors.
/// </summary>

// EN: Register MediatR / VI: Đăng ký MediatR
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    
    // EN: Pipeline behaviors (order matters!)
    // VI: Pipeline behaviors (thứ tự quan trọng!)
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
    cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
});

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

// EN: Register Dapper connection for queries
// VI: Đăng ký Dapper connection cho queries
builder.Services.AddScoped<IDbConnection>(_ =>
    new NpgsqlConnection(builder.Configuration.GetConnectionString("DefaultConnection")));
```

---

## Command Patterns

### Immutable Command Records

```csharp
/// <summary>
/// EN: Command to create an order. Commands should be immutable.
/// VI: Command tạo order. Commands nên là immutable.
/// </summary>
public record CreateOrderCommand : IRequest<OrderResult>
{
    public string UserId { get; init; }
    public Address ShippingAddress { get; init; }
    public IReadOnlyList<OrderItemDto> Items { get; init; }

    public CreateOrderCommand(
        string userId,
        Address shippingAddress,
        IReadOnlyList<OrderItemDto> items)
    {
        UserId = userId;
        ShippingAddress = shippingAddress;
        Items = items;
    }
}

/// <summary>
/// EN: Command to update order status.
/// VI: Command cập nhật trạng thái order.
/// </summary>
public record UpdateOrderStatusCommand(
    Guid OrderId,
    string UserId,
    OrderStatus NewStatus) : IRequest<OrderResult>;

/// <summary>
/// EN: Command to cancel order.
/// VI: Command hủy order.
/// </summary>
public record CancelOrderCommand(
    Guid OrderId,
    string UserId,
    string Reason) : IRequest<OrderResult>;
```

### Command Handler with Full Domain Logic

```csharp
/// <summary>
/// EN: Handler for CreateOrderCommand with domain validation.
/// VI: Handler cho CreateOrderCommand với domain validation.
/// </summary>
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderResult>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IProductService _productService;
    private readonly ILogger<CreateOrderCommandHandler> _logger;

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

    public async Task<OrderResult> Handle(
        CreateOrderCommand request,
        CancellationToken ct)
    {
        // EN: Validate products exist and get prices
        // VI: Xác thực products tồn tại và lấy giá
        var productIds = request.Items.Select(i => i.ProductId).ToList();
        var products = await _productService.GetByIdsAsync(productIds, ct);

        if (products.Count != productIds.Count)
        {
            var missingIds = productIds.Except(products.Select(p => p.Id));
            throw new NotFoundException("Products", string.Join(", ", missingIds));
        }

        // EN: Create order aggregate
        // VI: Tạo order aggregate
        var order = new Order(request.UserId, request.ShippingAddress);

        foreach (var item in request.Items)
        {
            var product = products.First(p => p.Id == item.ProductId);
            order.AddItem(product.Id, item.Quantity, product.Price);
        }

        // EN: Persist via repository
        // VI: Lưu qua repository
        await _orderRepository.AddAsync(order, ct);
        await _orderRepository.UnitOfWork.SaveChangesAsync(ct);

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

        return new OrderResult(order.Id, order.Status.ToString(), order.TotalAmount);
    }
}
```

### Command Validator

```csharp
/// <summary>
/// EN: FluentValidation validator for CreateOrderCommand.
/// VI: FluentValidation validator cho CreateOrderCommand.
/// </summary>
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.UserId)
            .NotEmpty().WithMessage("User ID is required");

        RuleFor(x => x.ShippingAddress)
            .NotNull().WithMessage("Shipping address is required")
            .SetValidator(new AddressValidator()!);

        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("At least one item is required")
            .Must(items => items.Count <= 100)
            .WithMessage("Maximum 100 items per order");

        RuleForEach(x => x.Items)
            .SetValidator(new OrderItemDtoValidator());
    }
}

public class AddressValidator : AbstractValidator<Address>
{
    public AddressValidator()
    {
        RuleFor(x => x.Street).NotEmpty().MaximumLength(200);
        RuleFor(x => x.City).NotEmpty().MaximumLength(100);
        RuleFor(x => x.PostalCode).NotEmpty().Matches(@"^\d{5}(-\d{4})?$");
        RuleFor(x => x.Country).NotEmpty().MaximumLength(100);
    }
}

public class OrderItemDtoValidator : AbstractValidator<OrderItemDto>
{
    public OrderItemDtoValidator()
    {
        RuleFor(x => x.ProductId).NotEmpty();
        RuleFor(x => x.Quantity).GreaterThan(0).LessThanOrEqualTo(1000);
        RuleFor(x => x.UnitPrice).GreaterThanOrEqualTo(0);
    }
}
```

---

## Query Patterns

### Optimized Query with Dapper

```csharp
/// <summary>
/// EN: Query for paginated user orders.
/// VI: Query cho orders phân trang của user.
/// </summary>
public record GetUserOrdersQuery(
    string UserId,
    int Skip = 0,
    int Take = 20,
    OrderStatus? StatusFilter = null) : IRequest<PagedResult<OrderSummaryDto>>;

/// <summary>
/// EN: Handler using Dapper for optimized reads.
/// VI: 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)
    {
        var whereClause = "WHERE o.UserId = @UserId";
        var parameters = new DynamicParameters();
        parameters.Add("UserId", request.UserId);
        parameters.Add("Skip", request.Skip);
        parameters.Add("Take", request.Take);

        if (request.StatusFilter.HasValue)
        {
            whereClause += " AND o.Status = @Status";
            parameters.Add("Status", request.StatusFilter.Value.ToString());
        }

        // EN: Count total
        // VI: Đếm tổng
        var countSql = $"SELECT COUNT(*) FROM Orders o {whereClause}";
        var total = await _connection.ExecuteScalarAsync<int>(countSql, parameters);

        // EN: Get data with pagination
        // VI: Lấy dữ liệu với phân trang
        var dataSql = $@"
            SELECT 
                o.Id,
                o.Status,
                o.TotalAmount,
                o.CreatedAt,
                (SELECT COUNT(*) FROM OrderItems WHERE OrderId = o.Id) as ItemCount
            FROM Orders o
            {whereClause}
            ORDER BY o.CreatedAt DESC
            OFFSET @Skip ROWS FETCH NEXT @Take ROWS ONLY";

        var orders = await _connection.QueryAsync<OrderSummaryDto>(dataSql, parameters);

        return new PagedResult<OrderSummaryDto>(
            orders.ToList(),
            total,
            request.Skip / request.Take + 1,
            request.Take);
    }
}
```

### Complex Query with Multi-Mapping

```csharp
/// <summary>
/// EN: Query for order detail with items.
/// VI: Query cho chi tiết order với items.
/// </summary>
public record GetOrderDetailQuery(
    Guid OrderId,
    string UserId) : IRequest<OrderDetailDto?>;

public class GetOrderDetailQueryHandler
    : IRequestHandler<GetOrderDetailQuery, OrderDetailDto?>
{
    private readonly IDbConnection _connection;

    public async Task<OrderDetailDto?> Handle(
        GetOrderDetailQuery request,
        CancellationToken ct)
    {
        const string sql = @"
            SELECT 
                o.Id, o.UserId, o.Status, o.TotalAmount, o.CreatedAt,
                o.ShippingAddress_Street as Street,
                o.ShippingAddress_City as City,
                o.ShippingAddress_PostalCode as PostalCode,
                o.ShippingAddress_Country as Country,
                oi.Id as ItemId, oi.ProductId, oi.Quantity, oi.UnitPrice,
                p.Name as ProductName
            FROM Orders o
            LEFT JOIN OrderItems oi ON o.Id = oi.OrderId
            LEFT JOIN Products p ON oi.ProductId = p.Id
            WHERE o.Id = @OrderId AND o.UserId = @UserId";

        var orderDict = new Dictionary<Guid, OrderDetailDto>();

        await _connection.QueryAsync<OrderDetailDto, OrderItemDetailDto?, OrderDetailDto>(
            sql,
            (order, item) =>
            {
                if (!orderDict.TryGetValue(order.Id, out var existingOrder))
                {
                    existingOrder = order;
                    existingOrder.Items = new List<OrderItemDetailDto>();
                    orderDict.Add(order.Id, existingOrder);
                }

                if (item != null)
                    existingOrder.Items.Add(item);

                return existingOrder;
            },
            new { request.OrderId, request.UserId },
            splitOn: "ItemId");

        return orderDict.Values.FirstOrDefault();
    }
}
```

---

## Pipeline Behaviors

### Complete Validation Behavior

```csharp
/// <summary>
/// EN: Validation behavior with detailed error mapping.
/// VI: Validation behavior với ánh xạ lỗi chi tiết.
/// </summary>
public class ValidationBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;
    private readonly ILogger<ValidationBehavior<TRequest, TResponse>> _logger;

    public ValidationBehavior(
        IEnumerable<IValidator<TRequest>> validators,
        ILogger<ValidationBehavior<TRequest, TResponse>> logger)
    {
        _validators = validators;
        _logger = logger;
    }

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

        if (!_validators.Any())
        {
            _logger.LogDebug("No validators for {RequestType}", typeName);
            return await next();
        }

        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, ct)));

        var failures = validationResults
            .SelectMany(r => r.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Count != 0)
        {
            _logger.LogWarning(
                "Validation failed for {RequestType}: {Errors}",
                typeName,
                string.Join(", ", failures.Select(f => f.ErrorMessage)));

            throw new ValidationException(failures);
        }

        return await next();
    }
}
```

### Logging Behavior with Performance Tracking

```csharp
/// <summary>
/// EN: Logging behavior with performance alerts.
/// VI: Logging behavior với cảnh báo hiệu năng.
/// </summary>
public class LoggingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
    private readonly TimeSpan _slowThreshold = TimeSpan.FromSeconds(3);

    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;
        var requestId = Guid.NewGuid().ToString("N")[..8];

        _logger.LogInformation(
            "[{RequestId}] Handling {RequestName}",
            requestId, requestName);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            var response = await next();
            stopwatch.Stop();

            if (stopwatch.Elapsed > _slowThreshold)
            {
                _logger.LogWarning(
                    "[{RequestId}] SLOW: {RequestName} took {ElapsedMs}ms",
                    requestId, requestName, stopwatch.ElapsedMilliseconds);
            }
            else
            {
                _logger.LogInformation(
                    "[{RequestId}] Completed {RequestName} in {ElapsedMs}ms",
                    requestId, requestName, stopwatch.ElapsedMilliseconds);
            }

            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(ex,
                "[{RequestId}] FAILED: {RequestName} after {ElapsedMs}ms",
                requestId, requestName, stopwatch.ElapsedMilliseconds);
            throw;
        }
    }
}
```

### Transaction Behavior

```csharp
/// <summary>
/// EN: Transaction behavior for commands.
/// VI: Transaction behavior cho commands.
/// </summary>
public class TransactionBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ApplicationDbContext _dbContext;
    private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger;

    public TransactionBehavior(
        ApplicationDbContext dbContext,
        ILogger<TransactionBehavior<TRequest, TResponse>> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        // EN: Only wrap commands in transactions
        // VI: Chỉ wrap commands trong transactions
        if (!IsCommand())
            return await next();

        var typeName = typeof(TRequest).Name;

        await using var transaction = await _dbContext.Database
            .BeginTransactionAsync(ct);

        try
        {
            _logger.LogDebug("Begin transaction for {RequestType}", typeName);

            var response = await next();

            await transaction.CommitAsync(ct);

            _logger.LogDebug("Committed transaction for {RequestType}", typeName);

            return response;
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync(ct);
            _logger.LogError(ex, "Rolled back transaction for {RequestType}", typeName);
            throw;
        }
    }

    private static bool IsCommand()
    {
        return typeof(TRequest).Name.EndsWith("Command");
    }
}
```

---

## Idempotency

### Identified Command Pattern

```csharp
/// <summary>
/// EN: Wrapper for idempotent commands.
/// VI: Wrapper cho commands idempotent.
/// </summary>
public record IdentifiedCommand<TCommand, TResult>(
    Guid RequestId,
    TCommand Command) : IRequest<TResult>
    where TCommand : IRequest<TResult>;

/// <summary>
/// EN: Handler for identified commands.
/// VI: Handler cho identified commands.
/// </summary>
public class IdentifiedCommandHandler<TCommand, TResult>
    : IRequestHandler<IdentifiedCommand<TCommand, TResult>, TResult>
    where TCommand : IRequest<TResult>
{
    private readonly IMediator _mediator;
    private readonly IRequestManager _requestManager;
    private readonly ILogger<IdentifiedCommandHandler<TCommand, TResult>> _logger;

    public IdentifiedCommandHandler(
        IMediator mediator,
        IRequestManager requestManager,
        ILogger<IdentifiedCommandHandler<TCommand, TResult>> logger)
    {
        _mediator = mediator;
        _requestManager = requestManager;
        _logger = logger;
    }

    public async Task<TResult> Handle(
        IdentifiedCommand<TCommand, TResult> request,
        CancellationToken ct)
    {
        var commandName = typeof(TCommand).Name;

        // EN: Check if already processed
        // VI: Kiểm tra đã xử lý chưa
        if (await _requestManager.ExistsAsync(request.RequestId, ct))
        {
            _logger.LogWarning(
                "Duplicate request {RequestId} for {CommandName}",
                request.RequestId, commandName);

            return default!; // EN: Return cached result if needed
        }

        // EN: Mark as processing
        // VI: Đánh dấu đang xử lý
        await _requestManager.CreateAsync(request.RequestId, commandName, ct);

        try
        {
            var result = await _mediator.Send(request.Command, ct);

            _logger.LogInformation(
                "Processed {CommandName} with RequestId {RequestId}",
                commandName, request.RequestId);

            return result;
        }
        catch
        {
            await _requestManager.DeleteAsync(request.RequestId, ct);
            throw;
        }
    }
}
```

### Request Manager

```csharp
/// <summary>
/// EN: Interface for request deduplication.
/// VI: Interface cho request deduplication.
/// </summary>
public interface IRequestManager
{
    Task<bool> ExistsAsync(Guid requestId, CancellationToken ct);
    Task CreateAsync(Guid requestId, string commandName, CancellationToken ct);
    Task DeleteAsync(Guid requestId, CancellationToken ct);
}

/// <summary>
/// EN: Database-backed request manager.
/// VI: Request manager với database.
/// </summary>
public class RequestManager : IRequestManager
{
    private readonly ApplicationDbContext _context;

    public RequestManager(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<bool> ExistsAsync(Guid requestId, CancellationToken ct)
    {
        return await _context.ClientRequests
            .AnyAsync(r => r.Id == requestId, ct);
    }

    public async Task CreateAsync(Guid requestId, string commandName, CancellationToken ct)
    {
        var request = new ClientRequest
        {
            Id = requestId,
            Name = commandName,
            Time = DateTime.UtcNow
        };

        _context.ClientRequests.Add(request);
        await _context.SaveChangesAsync(ct);
    }

    public async Task DeleteAsync(Guid requestId, CancellationToken ct)
    {
        var request = await _context.ClientRequests.FindAsync(new object[] { requestId }, ct);
        if (request != null)
        {
            _context.ClientRequests.Remove(request);
            await _context.SaveChangesAsync(ct);
        }
    }
}

/// <summary>
/// EN: Entity for tracking processed requests.
/// VI: Entity theo dõi requests đã xử lý.
/// </summary>
public class ClientRequest
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public DateTime Time { get; set; }
}
```

---

## Domain Events

### Publishing Domain Events

```csharp
/// <summary>
/// EN: Domain event interface.
/// VI: Interface domain event.
/// </summary>
public interface IDomainEvent : INotification
{
    DateTime OccurredOn { get; }
}

/// <summary>
/// EN: Order created domain event.
/// VI: Domain event order được tạo.
/// </summary>
public record OrderCreatedEvent(
    Guid OrderId,
    string UserId,
    decimal TotalAmount) : IDomainEvent
{
    public DateTime OccurredOn { get; } = DateTime.UtcNow;
}

/// <summary>
/// EN: Handler for OrderCreatedEvent.
/// VI: Handler cho OrderCreatedEvent.
/// </summary>
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderCreatedEventHandler> _logger;

    public OrderCreatedEventHandler(
        IEmailService emailService,
        ILogger<OrderCreatedEventHandler> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)
    {
        _logger.LogInformation(
            "Sending order confirmation email for Order {OrderId}",
            notification.OrderId);

        await _emailService.SendOrderConfirmationAsync(
            notification.UserId,
            notification.OrderId,
            notification.TotalAmount,
            ct);
    }
}
```

---

## Resources / Tài Nguyên

- [MediatR Documentation](https://github.com/jbogard/MediatR)
- [FluentValidation](https://docs.fluentvalidation.net/)
- [CQRS Pattern - Microsoft](https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs)
- [eShopOnContainers - CQRS](https://github.com/dotnet-architecture/eShopOnContainers)
