Cqrs patterns
Skill rynhardt-potgieter/sprint_workflow/plugins/sprint-workflow/skills/cqrs-patterns
A portable Claude Code plugin system for orchestrating software development through parallel specialist agents, enforced engineering standards, and automated quality gates. One command to plan. Parallel agents to build. Automated gates to ship.
npx -y skills add rynhardt-potgieter/sprint_workflow --skill cqrs-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
CQRS and MediatR patterns — command/query separation, handler design, pipeline behaviors, read/write model separation, domain events, and notification handlers. Use this skill when implementing MediatR commands or queries, designing CQRS pipelines, adding validation behaviors, or separating read and write concerns.
SKILL.md
7.4 KB, as published. Nobody here has run it
CQRS & MediatR Standards
This skill defines patterns for CQRS (Command Query Responsibility Segregation) using MediatR in .NET projects.
When to Use CQRS
Use CQRS When
- Read and write models have different shapes (list views vs edit forms)
- Write operations have complex validation or side effects
- You need audit trails on state changes
- Domain events should trigger downstream actions
- Multiple teams work on the same domain
Don't Use CQRS When
- Simple CRUD with identical read/write shapes
- No complex business rules
- Small domain with few entities
- Adding complexity outweighs the benefit
MediatR Setup
Registration
// Program.cs
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(AppDbContext).Assembly));
Project Structure
Core/
Commands/ # IRequest<TResult> command types
CreateGoal.cs # Contains Command record + Handler class
Queries/ # IRequest<TResult> query types
GetGoalById.cs # Contains Query record + Handler class
Events/ # INotification types for domain events
GoalCreated.cs
Command Pattern
Command + Handler (Same File)
// Commands/CreateGoal.cs
public record CreateGoalCommand(
string Name,
GoalType Type,
decimal TargetAmount,
DateOnly? Deadline,
string UserId
) : IRequest<ApiResponse<GoalDto>>;
public class CreateGoalHandler : IRequestHandler<CreateGoalCommand, ApiResponse<GoalDto>>
{
private readonly AppDbContext _db;
private readonly IPublisher _publisher;
public CreateGoalHandler(AppDbContext db, IPublisher publisher)
{
_db = db;
_publisher = publisher;
}
public async Task<ApiResponse<GoalDto>> Handle(
CreateGoalCommand request, CancellationToken ct)
{
var goal = new Goal
{
Name = request.Name,
Type = request.Type,
TargetAmount = request.TargetAmount,
Deadline = request.Deadline,
UserId = request.UserId,
};
_db.Goals.Add(goal);
await _db.SaveChangesAsync(ct);
await _publisher.Publish(new GoalCreatedEvent(goal.Id, goal.UserId), ct);
return ApiResponse.Ok(GoalDto.From(goal));
}
}
Command Rules
- Commands represent intent:
CreateGoal,ArchiveWorkflow,ApproveHumanTask - Commands are records (immutable)
- One handler per command
- Commands return results (not void) — caller needs to know success/failure
- Commands can publish domain events via
IPublisher
Query Pattern
// Queries/GetGoalById.cs
public record GetGoalByIdQuery(Guid GoalId, string UserId) : IRequest<GoalDto?>;
public class GetGoalByIdHandler : IRequestHandler<GetGoalByIdQuery, GoalDto?>
{
private readonly AppDbContext _db;
public GetGoalByIdHandler(AppDbContext db) => _db = db;
public async Task<GoalDto?> Handle(GetGoalByIdQuery request, CancellationToken ct)
{
return await _db.Goals
.AsNoTracking()
.Where(g => g.Id == request.GoalId && g.UserId == request.UserId)
.Select(g => new GoalDto(g.Id, g.Name, g.TargetAmount, g.CurrentAmount))
.FirstOrDefaultAsync(ct);
}
}
Query Rules
- Queries are read-only — NEVER modify state
- Use
AsNoTracking()for all query handlers - Project directly to DTOs — never return domain entities
- Filter by
UserId/TenantIdin the query (access control)
Domain Events
// Events/GoalCreated.cs
public record GoalCreatedEvent(Guid GoalId, string UserId) : INotification;
// Handlers can be in Infrastructure (for side effects)
public class GoalCreatedHandler : INotificationHandler<GoalCreatedEvent>
{
private readonly IXpService _xpService;
public GoalCreatedHandler(IXpService xpService) => _xpService = xpService;
public async Task Handle(GoalCreatedEvent notification, CancellationToken ct)
{
await _xpService.AwardXpAsync(notification.UserId, XpAction.GoalCreated, ct);
}
}
Event Rules
- Events are past tense:
GoalCreated,WorkflowCompleted,HumanTaskApproved - Events are notifications (
INotification) — multiple handlers can respond - Handlers should be idempotent (events may be replayed)
- Side-effecting handlers go in Infrastructure, not Core
Pipeline Behaviors
Validation Behavior
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)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await next();
}
}
Logging Behavior
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
_logger.LogInformation("Handling {RequestType}", typeof(TRequest).Name);
var response = await next();
_logger.LogInformation("Handled {RequestType}", typeof(TRequest).Name);
return response;
}
}
Registration Order
// Pipeline behaviors execute in registration order (outermost first)
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
// Request → Logging → Validation → Handler → Validation → Logging → Response
Controller Integration
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class GoalsController : ControllerBase
{
private readonly ISender _sender;
public GoalsController(ISender sender) => _sender = sender;
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateGoalRequest req, CancellationToken ct)
{
var command = new CreateGoalCommand(req.Name, req.Type, req.TargetAmount, req.Deadline, CurrentUserId);
var result = await _sender.Send(command, ct);
if (!result.Success) return BadRequest(result);
return CreatedAtAction(nameof(GetById), new { id = result.Data!.Id }, result);
}
}
Anti-Patterns
- Handler calling another handler via
ISender.Send()— compose in the service layer instead - Commands that return complex domain objects — return DTOs only
- Queries that modify state — strict read-only enforcement
- Monolithic handlers with 200+ lines — extract domain logic to services
- Missing
CancellationTokenpropagation through the pipeline - Publishing events before
SaveChangesAsync— data might not persist if save fails