--- paths: - "*Command*.cs" - "*Query*.cs" - "*Handler*.cs" - "**/Program.cs" - "**/ServiceCollectionExtensions.cs" --- # Commands & Queries ## Commands Are Domain Actions, Not CRUD Commands express **user intent**. Name them as the business action being performed. - `PlaceOrderCommand` not `CreateOrderCommand` - `ApproveExpenseCommand` not `UpdateExpenseCommand` - `DeactivateAccountCommand` not `DeleteAccountCommand` One CRUD "Update" often becomes multiple distinct commands, each with its own validation and side effects. ## File Structure Command, handler, and validator live in the **same file**, organized by feature: ```csharp // File: Features/Orders/PlaceOrderCommand.cs public record PlaceOrderCommand { public string CustomerId { get; set; } = string.Empty; public List Items { get; set; } = []; } public class PlaceOrderCommandValidator : AbstractValidator { public PlaceOrderCommandValidator() { RuleFor(x => x.CustomerId).NotEmpty(); RuleFor(x => x.Items).NotEmpty(); } } public class PlaceOrderCommandHandler : ICommandHandler { public Task HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken = default) { // implementation } } ``` ## Handler Interfaces ```csharp // Command with no result ICommandHandler Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) // Command with result ICommandHandler Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) // Query (always returns result) — only for single-entity lookups or non-queryable data IQueryHandler Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) ``` ## Registration ```csharp // Command without result services.AddCommand(); // Command with result services.AddCommand(); // Command with result + validator (from Svrnty.CQRS.FluentValidation) services.AddCommand(); // Regular query — ONLY for single-entity lookups or non-queryable results services.AddQuery(); ``` ## When to Use Regular IQueryHandler vs Dynamic Query **Use `IQueryHandler`** (rare): - Single entity by ID: `FetchOrderByIdQuery` - Non-entity results: `GetDashboardStatsQuery` - Complex aggregation not expressible as IQueryable **Use Dynamic Query** (default — see dynamic-query.md rule): - Any list/collection query - Anything that needs pagination, filtering, or sorting ## Rules - Always use `CancellationToken` — never omit it - Commands/queries are `record` types with `{ get; set; }` properties - Default string properties to `string.Empty`, collections to `[]` - Naming: endpoint name is auto-derived by stripping `Command`/`Query` suffix and converting to lowerCamelCase