---
name: net-cqrs
description: Implement CQRS pattern with MediatR for .NET applications
license: MIT
compatibility: opencode
metadata:
audience: .net-developers
framework: aspnetcore
patterns: cqrs, mediator, event-sourcing
---
## What I Do
I help you implement CQRS (Command Query Responsibility Segregation):
- Command handlers for mutations
- Query handlers for reads
- MediatR pipeline configuration
- Validation with FluentValidation
- Domain events publishing
- Event sourcing basics
## When to Use Me
Use this skill when:
- Implementing CQRS architecture
- Setting up MediatR in a Clean Architecture project
- Creating command and query handlers
- Separating read and write models
- Publishing domain events
## Packages Required
```xml
```
## Command Structure
### Base Command
```csharp
using MediatR;
public interface ICommand : IRequest
{
}
public interface ICommandHandler : IRequestHandler
where TCommand : ICommand
{
}
```
### Command Example
```csharp
public record CreateOrderCommand(
Guid CustomerId,
List Items,
string ShippingAddress
) : ICommand;
public class CreateOrderCommandHandler : ICommandHandler
{
private readonly IOrderRepository _orderRepository;
private readonly IMapper _mapper;
private readonly IPublisher _publisher;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
IMapper mapper,
IPublisher publisher)
{
_orderRepository = orderRepository;
_mapper = mapper;
_publisher = publisher;
}
public async Task Handle(
CreateOrderCommand request,
CancellationToken cancellationToken)
{
var order = Order.Create(
request.CustomerId,
request.Items.Select(i => new OrderItem(i.ProductId, i.Quantity, i.Price)),
request.ShippingAddress);
await _orderRepository.AddAsync(order, cancellationToken);
// Publish domain events
foreach (var domainEvent in order.DomainEvents)
{
await _publisher.Publish(domainEvent, cancellationToken);
}
order.ClearDomainEvents();
return _mapper.Map(order);
}
}
```
### Command Validator
```csharp
public class CreateOrderCommandValidator : AbstractValidator
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty()
.WithMessage("Customer ID is required");
RuleFor(x => x.Items)
.NotEmpty()
.WithMessage("At least one item is required");
RuleForEach(x => x.Items)
.ChildRules(item =>
{
item.RuleFor(x => x.Quantity)
.GreaterThan(0)
.WithMessage("Quantity must be greater than 0");
item.RuleFor(x => x.Price)
.GreaterThan(0)
.WithMessage("Price must be greater than 0");
});
RuleFor(x => x.ShippingAddress)
.NotEmpty()
.MaximumLength(500)
.WithMessage("Shipping address is required (max 500 characters)");
}
}
```
## Query Structure
### Base Query
```csharp
using MediatR;
public interface IQuery : IRequest
{
}
public interface IQueryHandler : IRequestHandler
where TQuery : IQuery
{
}
```
### Query Example
```csharp
public record GetOrderByIdQuery(Guid OrderId) : IQuery;
public class GetOrderByIdQueryHandler : IQueryHandler
{
private readonly IOrderReadRepository _orderReadRepository;
private readonly IMapper _mapper;
public GetOrderByIdQueryHandler(
IOrderReadRepository orderReadRepository,
IMapper mapper)
{
_orderReadRepository = orderReadRepository;
_mapper = mapper;
}
public async Task Handle(
GetOrderByIdQuery request,
CancellationToken cancellationToken)
{
var order = await _orderReadRepository.GetByIdAsync(
request.OrderId,
cancellationToken);
return order is null ? null : _mapper.Map(order);
}
}
```
### Paginated Query
```csharp
public record GetOrdersQuery(
Guid? CustomerId,
DateTime? FromDate,
DateTime? ToDate,
int Page = 1,
int PageSize = 10
) : IQuery>;
public class GetOrdersQueryHandler : IQueryHandler>
{
private readonly IOrderReadRepository _repository;
public GetOrdersQueryHandler(IOrderReadRepository repository)
{
_repository = repository;
}
public async Task> Handle(
GetOrdersQuery request,
CancellationToken cancellationToken)
{
return await _repository.GetPaginatedAsync(
request.CustomerId,
request.FromDate,
request.ToDate,
request.Page,
request.PageSize,
cancellationToken);
}
}
```
## Pipeline Behaviors
### Validation Behavior
```csharp
public class ValidationBehaviour : IPipelineBehavior
where TRequest : notnull
{
private readonly IEnumerable> _validators;
public ValidationBehaviour(IEnumerable> validators)
{
_validators = validators;
}
public async Task Handle(
TRequest request,
RequestHandlerDelegate next,
CancellationToken cancellationToken)
{
if (!_validators.Any())
return await next();
var context = new ValidationContext(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next();
}
}
```
### Transaction Behavior
```csharp
public class TransactionBehaviour : IPipelineBehavior
where TRequest : ICommand
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger> _logger;
public TransactionBehaviour(
IUnitOfWork unitOfWork,
ILogger> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task Handle(
TRequest request,
RequestHandlerDelegate next,
CancellationToken cancellationToken)
{
var typeName = typeof(TRequest).Name;
try
{
await _unitOfWork.BeginTransactionAsync(cancellationToken);
_logger.LogInformation("Begin transaction for {CommandName}", typeName);
var response = await next();
await _unitOfWork.CommitTransactionAsync(cancellationToken);
_logger.LogInformation("Committed transaction for {CommandName}", typeName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during transaction for {CommandName}", typeName);
await _unitOfWork.RollbackTransactionAsync(cancellationToken);
throw;
}
}
}
```
## Domain Events
### Domain Event Handler
```csharp
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 cancellationToken)
{
_logger.LogInformation(
"Processing OrderCreatedEvent for Order {OrderId}",
notification.OrderId);
await _emailService.SendOrderConfirmationAsync(
notification.OrderId,
notification.CustomerEmail,
cancellationToken);
}
}
```
## DI Registration
```csharp
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
// Add behaviors in order of execution
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehaviour<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehaviour<,>));
});
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
```
## Best Practices
1. **Separate Commands and Queries**: Keep read and write models separate
2. **Immutable Commands/Queries**: Use records for immutability
3. **Validation in Pipeline**: Use FluentValidation behaviors
4. **Transaction Management**: Wrap commands in transactions
5. **Event Publishing**: Publish domain events after successful commands
6. **Thin Handlers**: Keep handlers focused on orchestration
## Example Usage
```
Use net-cqrs skill to:
1. Create command with handler and validator
2. Create query with pagination support
3. Set up MediatR pipeline behaviors
4. Implement domain event handlers
5. Configure transaction behavior for commands
```
I will generate complete CQRS implementation following Clean Architecture principles.