Back to list
aiskillstore

cqs-patterns

by aiskillstore

Security-audited skills for Claude, Codex & Claude Code. One-click install, quality verified.

102🍴 3📅 Jan 23, 2026

SKILL.md


name: cqs-patterns description: Command Query Separation (CQS) and CQRS patterns for .NET. Use when designing methods, handlers, and application architecture. Ensures predictable, testable code. allowed-tools: Read, Grep, Glob, Edit, Write

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

BenefitDescription
TestabilityCommands and queries are isolated, easy to test
ScalabilityRead and write sides can scale independently
OptimizationRead models optimized for queries, write models for business rules
ClarityClear intent - methods either change state or return data
MaintainabilitySingle responsibility, easier to modify
DebuggingQueries 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

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

+5
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon