Claude
Skills
Sign in
Back

cqs-patterns

Included with Lifetime
$97 forever

Command Query Separation (CQS) and CQRS patterns for .NET. Use when designing methods, handlers, and application architecture. Ensures predictable, testable code.

General

What this skill does


# 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
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
// 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
```csharp
// 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

```csharp
// === 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

```csharp
// === 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

Related in General