.claude/skills/cqrs-standards/SKILL.md
Use when implementing or reviewing CQRS with MediatR — command handlers, query handlers, pipeline behaviors, or request/response patterns in .NET projects.
npx skillsauth add klod68/littlerae cqrs-standardsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Field | Value |
|---|---|
| Name | CQRS via MediatR Standards |
| Domain | CQRS, MediatR, Application Layer |
| Level | Feature |
| Tags | cqrs, mediatr, command, query, pipeline-behavior |
Activate this skill when the task involves:
These rules layer on top of base architectural standards. On conflict, these win.
<!-- SHARED:rules/cqrs.md -->Apply these rules in addition to the foundational principles for projects using MediatR. CQRS (Command Query Responsibility Segregation) is enforced at the Application layer. Every use case maps to either a Command or a Query — never both.
Commands change state. They do not return data beyond a status or a created ID. Queries return data. They do not change state. These two concerns MUST NEVER be mixed in a single handler.
Mixing the two makes it impossible to cache query results, replay commands for audit, or scale read and write paths independently. It also makes handlers harder to test — you cannot verify a state change without also verifying an expected return value, coupling two concerns in one assertion.
User Action
│
▼
┌────────────────────────────────────────────┐
│ MediatR Pipeline │
│ Logging → Validation → Transaction → ... │
└─────────────────┬──────────────────────────┘
│
┌─────────┴─────────┐
│ │
┌────▼────┐ ┌────▼────┐
│ Command │ │ Query │
│ Handler │ │ Handler │
└────┬────┘ └────┬────┘
│ │
Writes State Reads State
Returns Result<T> Returns DTO/List
| Item | Convention | Example |
|---|---|---|
| Command record | {Verb}{Subject}Command | Create{Entity}Command |
| Handler class | {Verb}{Subject}Handler | Create{Entity}Handler |
| Validator class | {Verb}{Subject}CommandValidator | Create{Entity}CommandValidator |
| Return type | Result<{Id or void}> | Result<{EntityId}> |
// Application/Commands/{FeatureName}/{CommandName}.cs
namespace {Solution}.Application.Commands.{FeatureName};
/// <summary>
/// Command to {verb + subject in present tense}.
/// </summary>
/// <remarks>UC-{NNN}</remarks>
public sealed record {CommandName}(
{PropertyType} {PropertyName} // one parameter per input, no mutable state
) : IRequest<Result<{TResult}>>;
Rules for Command records:
sealed record — not class, not mutable.Result<T> from FluentResults or equivalent.Result<{EntityId}>.Result (no value payload).// Application/Commands/{FeatureName}/{CommandName}Handler.cs
namespace {Solution}.Application.Commands.{FeatureName};
public sealed class {CommandName}Handler
: IRequestHandler<{CommandName}, Result<{TResult}>>
{
private readonly I{AggregateRepository} _repository;
public {CommandName}Handler(I{AggregateRepository} repository)
=> _repository = repository;
public async Task<Result<{TResult}>> Handle(
{CommandName} request, CancellationToken ct)
{
// 1. Load aggregate (if modifying existing)
// 2. Execute domain factory or domain method — ALL business logic lives here
// 3. Persist via repository
// 4. await _db.SaveChangesAsync(ct) ← exactly here, not in the repository
// 5. Return Result<T>
throw new NotImplementedException(); // Implementer fills this
}
}
Rules for Command handlers:
SaveChangesAsync is called here (or in a TransactionBehavior) — never in the repository.public sealed class {CommandName}Validator
: AbstractValidator<{CommandName}>
{
public {CommandName}Validator()
{
RuleFor(x => x.{PropertyName})
.NotEmpty()
.WithMessage("{PropertyName} is required.")
.MaximumLength({N})
.WithMessage("{PropertyName} must not exceed {N} characters.");
}
}
| Item | Convention | Example |
|---|---|---|
| Query record | Get{Subject}By{Criterion}Query or Get{Subjects}Query | Get{Entity}ByIdQuery |
| Handler class | Get{Subject}By{Criterion}Handler | Get{Entity}ByIdHandler |
| Result DTO | {Subject}Dto or {Subject}Response | {Entity}Dto |
| Return type | {ResultDto} or IReadOnlyList<{ResultDto}> or {ResultDto}? | |
public sealed class {QueryName}Handler
: IRequestHandler<{QueryName}, {QueryResult}>
{
private readonly I{ReadRepository} _repository;
public {QueryName}Handler(I{ReadRepository} repository)
=> _repository = repository;
public async Task<{QueryResult}> Handle(
{QueryName} request, CancellationToken ct)
{
// 1. Retrieve data via repository (read-only, AsNoTracking)
// 2. Map domain entity → DTO (never return the entity directly)
// 3. Return DTO
throw new NotImplementedException(); // Implementer fills this
}
}
Rules for Query handlers:
AsNoTracking() — always.null — do NOT throw.| Order | Behavior | Scope | Purpose |
|---|---|---|---|
| 1 | LoggingBehavior<,> | All requests | Structured request/response logging |
| 2 | ValidationBehavior<,> | All requests with a validator | FluentValidation execution, short-circuit on failure |
| 3 | TransactionBehavior<,> | Commands only | Wrap handler in a DB transaction, dispatch domain events post-commit |
SaveChangesAsync is called in exactly ONE place per request: the Command handler
or in the TransactionBehavior. Never in a repository method.
// Correct: in the handler
public async Task<Result<{EntityId}>> Handle(
Create{Entity}Command request, CancellationToken ct)
{
var result = {Entity}.Create(request.{Property});
if (result.IsFailure) return result.ToResult<{EntityId}>();
await _repository.AddAsync(result.Value, ct);
await _db.SaveChangesAsync(ct); // ← exactly here
return Result.Success(result.Value.Id);
}
// Wrong: in the repository — VIOLATION
public async Task AddAsync({Entity} entity, CancellationToken ct)
{
await _db.AddAsync(entity, ct);
await _db.SaveChangesAsync(ct); // BLOCKER
}
| Check | Violation | Severity |
|---|---|---|
| Command returns entity data | Command handler returns domain entity or full DTO | BLOCKER |
| Query modifies state | Query handler calls write repository or SaveChangesAsync | BLOCKER |
| Mixed handler | Single handler implements both command and query behavior | BLOCKER |
| SaveChangesAsync in repository | Write operation committed inside repository method | BLOCKER |
| Domain entity in query result | Handler returns entity instead of DTO | MAJOR |
| No validator for command | Command record with no corresponding AbstractValidator<> | MAJOR |
| Anti-Pattern | Fix |
|---|---|
| Handler that queries and commands | Split into separate Command + Query handlers |
| SaveChangesAsync in repository | Move to handler or TransactionBehavior |
| Returning full domain entity from query handler | Map to dedicated DTO |
| Validator with business rule logic | Move business rules to domain entity; keep validator for input shape/format |
| Re-ordering pipeline behaviors | Behaviors are position-sensitive; add to the end |
api-contracts.md — Interface-first design, return type abstractions, contract versioningdesign-patterns.md — Approved GoF patterns: Factory, Repository, Decorator, Strategy, Specificationresult-error-handling.md — Result object pattern, error constants, exception boundariesef-core.md — EF Core entity design, migrations, DbContext, querying, repository patternnaming.md — Naming conventions for types, methods, properties, namespacestdd.md — Test-driven development with xUnit, FluentAssertions, NSubstitutetools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.