# 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)