dotnet-cqrs/docs/getting-started/01-introduction.md

8.5 KiB

Introduction to CQRS

Learn what CQRS is, when to use it, and how Svrnty.CQRS implements the pattern.

What is CQRS?

CQRS stands for Command Query Responsibility Segregation. It's an architectural pattern that separates read operations (queries) from write operations (commands).

Traditional Approach

In traditional architectures, the same model handles both reads and writes:

// Traditional approach - same service for everything
public class UserService
{
    public void CreateUser(CreateUserDto dto) { /* write */ }
    public void UpdateUser(UpdateUserDto dto) { /* write */ }
    public UserDto GetUser(int id) { /* read */ }
    public List<UserDto> SearchUsers(string criteria) { /* read */ }
}

CQRS Approach

CQRS separates these responsibilities:

// Commands (write operations)
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
{
    public Task<int> HandleAsync(CreateUserCommand command, CancellationToken cancellationToken)
    {
        // Write logic only
    }
}

// Queries (read operations)
public class GetUserQueryHandler : IQueryHandler<GetUserQuery, UserDto>
{
    public Task<UserDto> HandleAsync(GetUserQuery query, CancellationToken cancellationToken)
    {
        // Read logic only
    }
}

Core Concepts

Commands

Commands represent write operations that change system state.

Characteristics:

  • Imperative names (CreateUser, UpdateOrder, DeleteProduct)
  • Contain all data needed for the operation
  • May or may not return a result
  • Can be validated before execution
  • Typically have side effects

Example:

public record PlaceOrderCommand
{
    public int CustomerId { get; init; }
    public List<OrderItem> Items { get; init; } = new();
    public decimal TotalAmount { get; init; }
}

Queries

Queries represent read operations that return data without changing state.

Characteristics:

  • Question-based names (GetUser, SearchOrders, FetchProducts)
  • Never modify state
  • Always return data
  • Can be cached
  • Should be idempotent

Example:

public record GetOrderQuery
{
    public int OrderId { get; init; }
}

Handlers

Handlers contain the actual business logic for commands and queries.

Command Handler:

public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, int>
{
    public async Task<int> HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken)
    {
        // Validate business rules
        // Save to database
        // Emit events
        // Return order ID
        return orderId;
    }
}

Query Handler:

public class GetOrderQueryHandler : IQueryHandler<GetOrderQuery, OrderDto>
{
    public async Task<OrderDto> HandleAsync(GetOrderQuery query, CancellationToken cancellationToken)
    {
        // Fetch from database
        // Map to DTO
        // Return data
        return orderDto;
    }
}

Why Use CQRS?

Benefits

  1. Separation of Concerns

    • Commands focus on business logic and validation
    • Queries focus on data retrieval and formatting
    • Easier to understand and maintain
  2. Scalability

    • Scale reads and writes independently
    • Optimize databases differently (write DB vs read DB)
    • Use read replicas for queries
  3. Flexibility

    • Different models for reading and writing
    • Optimize queries without affecting commands
    • Easy to add new queries without changing commands
  4. Security

    • Fine-grained authorization (per command/query)
    • Easier to audit write operations
    • Clear boundaries for access control
  5. Testing

    • Handlers are easy to unit test
    • Clear inputs and outputs
    • Mock dependencies easily
  6. Maintainability

    • Small, focused handlers
    • Single Responsibility Principle
    • Easy to add new features

Trade-offs

  1. Increased Complexity

    • More files and classes
    • Learning curve for team
    • Might be overkill for simple CRUD
  2. Consistency Challenges

    • With separate read/write models, eventual consistency may be required
    • Requires careful design
  3. Code Duplication

    • Some logic might be repeated
    • More boilerplate code

When to Use CQRS

Good Fit

  • Complex business logic - Commands with validation, rules, and workflows
  • Different read/write patterns - Complex queries vs simple writes
  • High scalability needs - Read-heavy or write-heavy systems
  • Audit requirements - Need to track all changes
  • Event sourcing - Natural fit with event-driven architectures
  • Microservices - Clear boundaries between services
  • Simple CRUD - Basic create/read/update/delete operations
  • Small applications - Overhead not justified
  • Tight deadlines - Team not familiar with pattern
  • Consistent data models - Same model for reads and writes

How Svrnty.CQRS Works

Svrnty.CQRS provides a lightweight, production-ready implementation:

1. Define Commands and Queries

// Just POCOs (Plain Old CLR Objects)
public record CreateProductCommand
{
    public string Name { get; init; } = string.Empty;
    public decimal Price { get; init; }
}

2. Implement Handlers

public class CreateProductCommandHandler : ICommandHandler<CreateProductCommand, int>
{
    private readonly IProductRepository _repository;

    public CreateProductCommandHandler(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task<int> HandleAsync(CreateProductCommand command, CancellationToken cancellationToken)
    {
        var product = new Product { Name = command.Name, Price = command.Price };
        await _repository.AddAsync(product, cancellationToken);
        return product.Id;
    }
}

3. Register in DI

builder.Services.AddCommand<CreateProductCommand, int, CreateProductCommandHandler>();

4. Automatic Endpoint Generation

Svrnty.CQRS automatically creates HTTP or gRPC endpoints:

HTTP:

POST /api/command/createProduct

gRPC:

rpc CreateProduct (CreateProductRequest) returns (CreateProductResponse);

5. Built-in Features

  • Validation - FluentValidation integration
  • Discovery - Metadata-driven endpoint generation
  • Authorization - Custom authorization services
  • Protocols - HTTP (Minimal API) and gRPC support
  • Dynamic Queries - OData-like filtering
  • Event Streaming - Event sourcing and projections

Architecture Overview

┌─────────────────┐
│   HTTP/gRPC     │  ← Automatic endpoint generation
│   Endpoints     │
└────────┬────────┘
         │
┌────────▼────────┐
│   Validation    │  ← FluentValidation
│   (Optional)    │
└────────┬────────┘
         │
┌────────▼────────┐
│    Handler      │  ← Your business logic
│  (Command/Query)│
└────────┬────────┘
         │
┌────────▼────────┐
│  Data Layer     │  ← Database, external APIs, etc.
│  (Your choice)  │
└─────────────────┘

Key Principles in Svrnty.CQRS

  1. Convention over Configuration

    • Minimal setup required
    • Automatic endpoint naming
    • Sensible defaults
  2. Metadata-Driven Discovery

    • Handlers registered as metadata
    • Runtime enumeration for endpoint generation
    • Type-safe at compile time
  3. Framework Agnostic

    • Works with any data access layer (EF Core, Dapper, etc.)
    • No prescribed database or ORM
    • Integration points are interfaces
  4. Production Ready

    • Validation, authorization, observability
    • Health checks, metrics, structured logging
    • Event sourcing and consumer groups

What's Next?

Now that you understand CQRS, let's get your development environment set up!

Continue to Installation

See Also