skills/architecture/cqrs-basics/SKILL.md
Use when implementing CQRS with separate read/write models and MediatR pipeline behaviors.
npx skillsauth add faysilalshareef/dotnet-ai-kit cqrs-basicsInstall 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.
// Commands represent intent to change state
// Naming: {Verb}{Noun}Command
public sealed record CreateOrderCommand(
string CustomerName,
List<CreateOrderCommand.OrderItemDto> Items) : IRequest<Result<Guid>>
{
public sealed record OrderItemDto(Guid ProductId, int Quantity);
}
public sealed record UpdateOrderStatusCommand(
Guid OrderId,
OrderStatus NewStatus) : IRequest<Result>;
public sealed record DeleteOrderCommand(Guid OrderId) : IRequest<Result>;
// Queries represent requests for data — never modify state
// Naming: Get{Noun}Query or List{Noun}Query
public sealed record GetOrderQuery(Guid OrderId)
: IRequest<OrderResponse?>;
public sealed record ListOrdersQuery(
string? CustomerName,
OrderStatus? Status,
int Page = 1,
int PageSize = 20) : IRequest<PagedList<OrderSummaryResponse>>;
internal sealed class CreateOrderCommandHandler(
IOrderRepository repository,
IUnitOfWork unitOfWork,
ILogger<CreateOrderCommandHandler> logger)
: IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(request.CustomerName);
foreach (var item in request.Items)
order.AddItem(item.ProductId, item.Quantity);
repository.Add(order);
await unitOfWork.SaveChangesAsync(ct);
logger.LogInformation("Order {OrderId} created", order.Id);
return Result<Guid>.Success(order.Id);
}
}
internal sealed class ListOrdersQueryHandler(AppDbContext db)
: IRequestHandler<ListOrdersQuery, PagedList<OrderSummaryResponse>>
{
public async Task<PagedList<OrderSummaryResponse>> Handle(
ListOrdersQuery request, CancellationToken ct)
{
var query = db.Orders.AsNoTracking();
if (!string.IsNullOrEmpty(request.CustomerName))
query = query.Where(o =>
o.CustomerName.Contains(request.CustomerName));
if (request.Status.HasValue)
query = query.Where(o => o.Status == request.Status.Value);
var totalCount = await query.CountAsync(ct);
var items = await query
.OrderByDescending(o => o.CreatedAt)
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.Select(o => new OrderSummaryResponse(
o.Id, o.CustomerName, o.Total, o.Status.ToString()))
.ToListAsync(ct);
return new PagedList<OrderSummaryResponse>(
items, totalCount, request.Page, request.PageSize);
}
}
public sealed record OrderResponse(
Guid Id,
string CustomerName,
decimal Total,
string Status,
DateTimeOffset CreatedAt,
List<OrderItemResponse> Items);
public sealed record OrderSummaryResponse(
Guid Id, string CustomerName, decimal Total, string Status);
public sealed record OrderItemResponse(
Guid ProductId, string ProductName, int Quantity, decimal UnitPrice);
public sealed record PagedList<T>(
List<T> Items,
int TotalCount,
int Page,
int PageSize)
{
public int TotalPages =>
(int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasNext => Page < TotalPages;
public bool HasPrevious => Page > 1;
}
// Program.cs
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
// Pipeline behaviors (order matters: first = outermost)
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>),
typeof(ValidationBehavior<,>));
});
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
// Minimal API
app.MapPost("/orders", async (
CreateOrderCommand cmd, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(cmd, ct);
return result.Match(
id => Results.Created($"/orders/{id}", new { id }),
error => Results.BadRequest(error.ToProblemDetails()));
});
app.MapGet("/orders", async (
[AsParameters] ListOrdersQuery query,
ISender sender, CancellationToken ct) =>
{
return Results.Ok(await sender.Send(query, ct));
});
// Controller
[ApiController]
[Route("api/orders")]
public sealed class OrdersController(ISender sender) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> Create(
CreateOrderCommand command, CancellationToken ct)
{
var result = await sender.Send(command, ct);
return result.Match<IActionResult>(
id => CreatedAtAction(nameof(Get), new { id }, null),
error => BadRequest(error.ToProblemDetails()));
}
[HttpGet("{id:guid}")]
public async Task<IActionResult> Get(Guid id, CancellationToken ct)
{
var result = await sender.Send(new GetOrderQuery(id), ct);
return result is not null ? Ok(result) : NotFound();
}
}
// Clean Architecture + CQRS
Application/
Orders/
Commands/
CreateOrder/
CreateOrderCommand.cs
CreateOrderCommandHandler.cs
CreateOrderCommandValidator.cs
Queries/
GetOrder/
GetOrderQuery.cs
GetOrderQueryHandler.cs
ListOrders/
ListOrdersQuery.cs
ListOrdersQueryHandler.cs
// VSA + CQRS
Features/
Orders/
CreateOrder.cs # Command + Handler + Validator in one file
GetOrder.cs # Query + Handler in one file
ListOrders.cs # Query + Handler in one file
IRequest< and IRequestHandler< implementationsCommand and Query suffixes in class namesMediatR package reference in .csprojCommands/ and Queries/ folder structureIPipelineBehavior< implementations{Verb}{Noun}Command / Get{Noun}QueryAsNoTracking() in all query handlers| Scenario | Use | |----------|-----| | Creating/updating data | Command | | Reading/listing data | Query | | Cross-cutting validation | Pipeline behavior | | Cross-cutting logging | Pipeline behavior | | Transaction wrapping | Pipeline behavior on commands only |
data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.