# 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 ``` ### DI Registration ```csharp /// /// EN: Configure MediatR with all behaviors. /// VI: Cấu hình MediatR với tất cả behaviors. /// // 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(_ => new NpgsqlConnection(builder.Configuration.GetConnectionString("DefaultConnection"))); ``` --- ## Command Patterns ### Immutable Command Records ```csharp /// /// EN: Command to create an order. Commands should be immutable. /// VI: Command tạo order. Commands nên là immutable. /// public record CreateOrderCommand : IRequest { public string UserId { get; init; } public Address ShippingAddress { get; init; } public IReadOnlyList Items { get; init; } public CreateOrderCommand( string userId, Address shippingAddress, IReadOnlyList items) { UserId = userId; ShippingAddress = shippingAddress; Items = items; } } /// /// EN: Command to update order status. /// VI: Command cập nhật trạng thái order. /// public record UpdateOrderStatusCommand( Guid OrderId, string UserId, OrderStatus NewStatus) : IRequest; /// /// EN: Command to cancel order. /// VI: Command hủy order. /// public record CancelOrderCommand( Guid OrderId, string UserId, string Reason) : IRequest; ``` ### Command Handler with Full Domain Logic ```csharp /// /// EN: Handler for CreateOrderCommand with domain validation. /// VI: Handler cho CreateOrderCommand với domain validation. /// public class CreateOrderCommandHandler : IRequestHandler { private readonly IOrderRepository _orderRepository; private readonly IProductService _productService; private readonly ILogger _logger; public CreateOrderCommandHandler( IOrderRepository orderRepository, IProductService productService, ILogger logger) { _orderRepository = orderRepository; _productService = productService; _logger = logger; } public async Task 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 /// /// EN: FluentValidation validator for CreateOrderCommand. /// VI: FluentValidation validator cho CreateOrderCommand. /// public class CreateOrderCommandValidator : AbstractValidator { 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
{ 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 { 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 /// /// EN: Query for paginated user orders. /// VI: Query cho orders phân trang của user. /// public record GetUserOrdersQuery( string UserId, int Skip = 0, int Take = 20, OrderStatus? StatusFilter = null) : IRequest>; /// /// EN: Handler using Dapper for optimized reads. /// VI: Handler dùng Dapper cho đọc tối ưu. /// public class GetUserOrdersQueryHandler : IRequestHandler> { private readonly IDbConnection _connection; public GetUserOrdersQueryHandler(IDbConnection connection) { _connection = connection; } public async Task> 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(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(dataSql, parameters); return new PagedResult( orders.ToList(), total, request.Skip / request.Take + 1, request.Take); } } ``` ### Complex Query with Multi-Mapping ```csharp /// /// EN: Query for order detail with items. /// VI: Query cho chi tiết order với items. /// public record GetOrderDetailQuery( Guid OrderId, string UserId) : IRequest; public class GetOrderDetailQueryHandler : IRequestHandler { private readonly IDbConnection _connection; public async Task 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(); await _connection.QueryAsync( sql, (order, item) => { if (!orderDict.TryGetValue(order.Id, out var existingOrder)) { existingOrder = order; existingOrder.Items = new List(); 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 /// /// EN: Validation behavior with detailed error mapping. /// VI: Validation behavior với ánh xạ lỗi chi tiết. /// public class ValidationBehavior : IPipelineBehavior where TRequest : IRequest { private readonly IEnumerable> _validators; private readonly ILogger> _logger; public ValidationBehavior( IEnumerable> validators, ILogger> logger) { _validators = validators; _logger = logger; } public async Task Handle( TRequest request, RequestHandlerDelegate 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(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 /// /// EN: Logging behavior with performance alerts. /// VI: Logging behavior với cảnh báo hiệu năng. /// public class LoggingBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ILogger> _logger; private readonly TimeSpan _slowThreshold = TimeSpan.FromSeconds(3); public LoggingBehavior(ILogger> logger) { _logger = logger; } public async Task Handle( TRequest request, RequestHandlerDelegate 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 /// /// EN: Transaction behavior for commands. /// VI: Transaction behavior cho commands. /// public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { private readonly ApplicationDbContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( ApplicationDbContext dbContext, ILogger> logger) { _dbContext = dbContext; _logger = logger; } public async Task Handle( TRequest request, RequestHandlerDelegate 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 /// /// EN: Wrapper for idempotent commands. /// VI: Wrapper cho commands idempotent. /// public record IdentifiedCommand( Guid RequestId, TCommand Command) : IRequest where TCommand : IRequest; /// /// EN: Handler for identified commands. /// VI: Handler cho identified commands. /// public class IdentifiedCommandHandler : IRequestHandler, TResult> where TCommand : IRequest { private readonly IMediator _mediator; private readonly IRequestManager _requestManager; private readonly ILogger> _logger; public IdentifiedCommandHandler( IMediator mediator, IRequestManager requestManager, ILogger> logger) { _mediator = mediator; _requestManager = requestManager; _logger = logger; } public async Task Handle( IdentifiedCommand 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 /// /// EN: Interface for request deduplication. /// VI: Interface cho request deduplication. /// public interface IRequestManager { Task ExistsAsync(Guid requestId, CancellationToken ct); Task CreateAsync(Guid requestId, string commandName, CancellationToken ct); Task DeleteAsync(Guid requestId, CancellationToken ct); } /// /// EN: Database-backed request manager. /// VI: Request manager với database. /// public class RequestManager : IRequestManager { private readonly ApplicationDbContext _context; public RequestManager(ApplicationDbContext context) { _context = context; } public async Task 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); } } } /// /// EN: Entity for tracking processed requests. /// VI: Entity theo dõi requests đã xử lý. /// 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 /// /// EN: Domain event interface. /// VI: Interface domain event. /// public interface IDomainEvent : INotification { DateTime OccurredOn { get; } } /// /// EN: Order created domain event. /// VI: Domain event order được tạo. /// public record OrderCreatedEvent( Guid OrderId, string UserId, decimal TotalAmount) : IDomainEvent { public DateTime OccurredOn { get; } = DateTime.UtcNow; } /// /// EN: Handler for OrderCreatedEvent. /// VI: Handler cho OrderCreatedEvent. /// public class OrderCreatedEventHandler : INotificationHandler { private readonly IEmailService _emailService; private readonly ILogger _logger; public OrderCreatedEventHandler( IEmailService emailService, ILogger 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)