agentsclimarketplace

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.

Install
npx -y skills add rynhardt-potgieter/sprint_workflow --skill cqrs-patterns

Assembled 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 / TenantId in 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 CancellationToken propagation through the pipeline
  • Publishing events before SaveChangesAsync — data might not persist if save fails

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.