Marketplace cqs-patterns
Command Query Separation (CQS) and CQRS patterns for .NET. Use when designing methods, handlers, and application architecture. Ensures predictable, testable code.
install
source · Clone the upstream repo
git clone https://github.com/aiskillstore/marketplace
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/doubleslashse/cqs-patterns" ~/.claude/skills/aiskillstore-marketplace-cqs-patterns && rm -rf "$T"
manifest:
skills/doubleslashse/cqs-patterns/SKILL.mdsource content
Command Query Separation (CQS)
The Principle
A method should either be a Command that performs an action, or a Query that returns data, but not both.
┌─────────────────────────────────────────────────────────┐ │ METHOD │ ├───────────────────────┬─────────────────────────────────┤ │ COMMAND │ QUERY │ ├───────────────────────┼─────────────────────────────────┤ │ - Changes state │ - Returns data │ │ - Returns void │ - No side effects │ │ - "Do something" │ - "Tell me something" │ │ - Imperative verbs │ - Noun or question │ │ (Create, Update, │ (Get, Find, Is, Has, │ │ Delete, Process) │ Calculate, Count) │ └───────────────────────┴─────────────────────────────────┘
CQS at Method Level
Violation Examples
// BAD: Method both modifies state AND returns data public class Stack<T> { public T Pop() // Both removes item AND returns it { var item = _items[_count - 1]; _count--; return item; } } // BAD: Getter with side effects public class Counter { private int _value; public int Value { get { return _value++; } // Query that modifies state! } } // BAD: Command returns data public class UserService { public User CreateUser(string email, string name) // Returns created user { var user = new User { Email = email, Name = name }; _repository.Add(user); return user; // CQS violation } }
Correct CQS Implementation
// GOOD: Separated commands and queries public class Stack<T> { // Query - returns data, no side effects public T Peek() { if (_count == 0) throw new InvalidOperationException("Stack is empty"); return _items[_count - 1]; } // Command - modifies state, returns void public void Pop() { if (_count == 0) throw new InvalidOperationException("Stack is empty"); _count--; } // Usage: separate calls var top = stack.Peek(); stack.Pop(); } // GOOD: Counter with pure query public class Counter { private int _value; public int Value => _value; // Pure query public void Increment() => _value++; // Command } // GOOD: User service with CQS public class UserService { // Command - creates user, returns identifier only public Guid CreateUser(string email, string name) { var userId = Guid.NewGuid(); var user = new User { Id = userId, Email = email, Name = name }; _repository.Add(user); return userId; // Returning ID is acceptable } // Query - retrieves user public User? GetUser(Guid userId) { return _repository.GetById(userId); } }
CQS Exceptions
Some situations justify combining command and query:
1. Atomic Operations
// Acceptable: Interlocked operations need to be atomic public int IncrementAndGet() { return Interlocked.Increment(ref _counter); } // Acceptable: Compare-and-swap patterns public bool TryUpdate(int expected, int newValue) { return Interlocked.CompareExchange(ref _value, newValue, expected) == expected; }
2. Fluent APIs
// Acceptable: Builder pattern returns this public class QueryBuilder { public QueryBuilder Where(string condition) { _conditions.Add(condition); return this; // Returns modified builder } public QueryBuilder OrderBy(string column) { _orderBy = column; return this; } }
3. Factory Methods
// Acceptable: Creation returns the created object public static Order Create(int customerId, IEnumerable<OrderItem> items) { return new Order { Id = Guid.NewGuid(), CustomerId = customerId, Items = items.ToList() }; }
CQRS - Command Query Responsibility Segregation
CQRS applies CQS at the architectural level, using separate models for reads and writes.
┌─────────────────────────────────────┐ │ APPLICATION │ └─────────────────────────────────────┘ │ ┌───────────────┴───────────────┐ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ COMMANDS │ │ QUERIES │ │ (Write) │ │ (Read) │ └───────────────┘ └───────────────┘ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Command │ │ Query │ │ Handlers │ │ Handlers │ └───────────────┘ └───────────────┘ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Domain Model │ │ Read Model │ │ (Rich) │ │ (DTOs) │ └───────────────┘ └───────────────┘ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ Write DB │──────────────▶│ Read DB │ │ (Normalized) │ Sync/Events │ (Optimized) │ └───────────────┘ └───────────────┘
Commands and Queries
// === MARKER INTERFACES === public interface ICommand { } public interface ICommand<TResult> { } public interface IQuery<TResult> { } // === COMMANDS === public record CreateOrderCommand( int CustomerId, List<OrderItemDto> Items ) : ICommand<Guid>; public record UpdateOrderStatusCommand( Guid OrderId, OrderStatus NewStatus ) : ICommand; public record CancelOrderCommand(Guid OrderId) : ICommand; // === QUERIES === public record GetOrderByIdQuery(Guid OrderId) : IQuery<OrderDto?>; public record GetOrdersByCustomerQuery( int CustomerId, int Page = 1, int PageSize = 10 ) : IQuery<PagedResult<OrderSummaryDto>>; public record GetOrderStatisticsQuery( DateTime From, DateTime To ) : IQuery<OrderStatisticsDto>;
Command Handlers
// === HANDLER INTERFACES === public interface ICommandHandler<in TCommand> where TCommand : ICommand { Task HandleAsync(TCommand command, CancellationToken ct = default); } public interface ICommandHandler<in TCommand, TResult> where TCommand : ICommand<TResult> { Task<TResult> HandleAsync(TCommand command, CancellationToken ct = default); } // === IMPLEMENTATION === public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid> { private readonly IOrderRepository _orderRepository; private readonly ICustomerRepository _customerRepository; private readonly IEventPublisher _eventPublisher; public CreateOrderCommandHandler( IOrderRepository orderRepository, ICustomerRepository customerRepository, IEventPublisher eventPublisher) { _orderRepository = orderRepository; _customerRepository = customerRepository; _eventPublisher = eventPublisher; } public async Task<Guid> HandleAsync(CreateOrderCommand command, CancellationToken ct) { // Validate var customer = await _customerRepository.GetByIdAsync(command.CustomerId, ct); if (customer == null) throw new CustomerNotFoundException(command.CustomerId); // Create domain entity var order = Order.Create(command.CustomerId); foreach (var item in command.Items) { order.AddItem(item.ProductId, item.Quantity, item.UnitPrice); } // Persist await _orderRepository.AddAsync(order, ct); // Publish domain event await _eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id), ct); return order.Id; } } public class CancelOrderCommandHandler : ICommandHandler<CancelOrderCommand> { private readonly IOrderRepository _orderRepository; private readonly IEventPublisher _eventPublisher; public async Task HandleAsync(CancelOrderCommand command, CancellationToken ct) { var order = await _orderRepository.GetByIdAsync(command.OrderId, ct); if (order == null) throw new OrderNotFoundException(command.OrderId); order.Cancel(); await _orderRepository.UpdateAsync(order, ct); await _eventPublisher.PublishAsync(new OrderCancelledEvent(order.Id), ct); } }
Query Handlers
public interface IQueryHandler<in TQuery, TResult> where TQuery : IQuery<TResult> { Task<TResult> HandleAsync(TQuery query, CancellationToken ct = default); } public class GetOrderByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, OrderDto?> { private readonly IDbConnection _connection; public GetOrderByIdQueryHandler(IDbConnection connection) { _connection = connection; } public async Task<OrderDto?> HandleAsync(GetOrderByIdQuery query, CancellationToken ct) { // Direct SQL for optimized reads const string sql = @" SELECT o.Id, o.CustomerId, o.Status, o.Total, o.CreatedAt, c.Name as CustomerName, c.Email as CustomerEmail FROM Orders o JOIN Customers c ON o.CustomerId = c.Id WHERE o.Id = @OrderId"; return await _connection.QuerySingleOrDefaultAsync<OrderDto>( new CommandDefinition(sql, new { query.OrderId }, cancellationToken: ct)); } } public class GetOrdersByCustomerQueryHandler : IQueryHandler<GetOrdersByCustomerQuery, PagedResult<OrderSummaryDto>> { private readonly IDbConnection _connection; public async Task<PagedResult<OrderSummaryDto>> HandleAsync( GetOrdersByCustomerQuery query, CancellationToken ct) { const string sql = @" SELECT Id, Status, Total, CreatedAt FROM Orders WHERE CustomerId = @CustomerId ORDER BY CreatedAt DESC OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY; SELECT COUNT(*) FROM Orders WHERE CustomerId = @CustomerId;"; using var multi = await _connection.QueryMultipleAsync( new CommandDefinition(sql, new { query.CustomerId, Offset = (query.Page - 1) * query.PageSize, query.PageSize }, cancellationToken: ct)); var items = (await multi.ReadAsync<OrderSummaryDto>()).ToList(); var totalCount = await multi.ReadSingleAsync<int>(); return new PagedResult<OrderSummaryDto>(items, query.Page, query.PageSize, totalCount); } }
Dispatcher Pattern
public interface IDispatcher { Task<TResult> SendAsync<TResult>(ICommand<TResult> command, CancellationToken ct = default); Task SendAsync(ICommand command, CancellationToken ct = default); Task<TResult> QueryAsync<TResult>(IQuery<TResult> query, CancellationToken ct = default); } public class Dispatcher : IDispatcher { private readonly IServiceProvider _serviceProvider; public Dispatcher(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task<TResult> SendAsync<TResult>(ICommand<TResult> command, CancellationToken ct) { var handlerType = typeof(ICommandHandler<,>).MakeGenericType(command.GetType(), typeof(TResult)); dynamic handler = _serviceProvider.GetRequiredService(handlerType); return await handler.HandleAsync((dynamic)command, ct); } public async Task SendAsync(ICommand command, CancellationToken ct) { var handlerType = typeof(ICommandHandler<>).MakeGenericType(command.GetType()); dynamic handler = _serviceProvider.GetRequiredService(handlerType); await handler.HandleAsync((dynamic)command, ct); } public async Task<TResult> QueryAsync<TResult>(IQuery<TResult> query, CancellationToken ct) { var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult)); dynamic handler = _serviceProvider.GetRequiredService(handlerType); return await handler.HandleAsync((dynamic)query, ct); } }
Usage in Controllers
[ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly IDispatcher _dispatcher; public OrdersController(IDispatcher dispatcher) { _dispatcher = dispatcher; } [HttpPost] public async Task<ActionResult<Guid>> CreateOrder( CreateOrderRequest request, CancellationToken ct) { var command = new CreateOrderCommand(request.CustomerId, request.Items); var orderId = await _dispatcher.SendAsync(command, ct); return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId); } [HttpGet("{id}")] public async Task<ActionResult<OrderDto>> GetOrder(Guid id, CancellationToken ct) { var query = new GetOrderByIdQuery(id); var order = await _dispatcher.QueryAsync(query, ct); return order == null ? NotFound() : Ok(order); } [HttpPost("{id}/cancel")] public async Task<IActionResult> CancelOrder(Guid id, CancellationToken ct) { var command = new CancelOrderCommand(id); await _dispatcher.SendAsync(command, ct); return NoContent(); } }
Benefits of CQS/CQRS
| Benefit | Description |
|---|---|
| Testability | Commands and queries are isolated, easy to test |
| Scalability | Read and write sides can scale independently |
| Optimization | Read models optimized for queries, write models for business rules |
| Clarity | Clear intent - methods either change state or return data |
| Maintainability | Single responsibility, easier to modify |
| Debugging | Queries are safe to call repeatedly |
When to Use CQRS
Good Fit
- Complex domains with different read/write patterns
- High-read, low-write scenarios
- Systems requiring audit trails
- Event-sourced systems
- Microservices with separate read replicas
Not Needed
- Simple CRUD applications
- Small teams/projects
- Consistent read/write patterns
- When added complexity outweighs benefits
Quick Reference
// Pure Query - Safe, no side effects public Customer? GetCustomer(int id); public IReadOnlyList<Order> FindOrdersByStatus(OrderStatus status); public bool IsEmailAvailable(string email); public int CountActiveUsers(); public decimal CalculateTotalRevenue(DateTime from, DateTime to); // Pure Command - Changes state, returns void (or ID) public void CreateCustomer(Customer customer); public void UpdateOrderStatus(Guid orderId, OrderStatus status); public void DeleteProduct(int productId); public Guid PlaceOrder(OrderRequest request); // ID return is acceptable