313 lines
8.5 KiB
Markdown
313 lines
8.5 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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:**
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
public record GetOrderQuery
|
|
{
|
|
public int OrderId { get; init; }
|
|
}
|
|
```
|
|
|
|
### Handlers
|
|
|
|
Handlers contain the actual business logic for commands and queries.
|
|
|
|
**Command Handler:**
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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
|
|
|
|
### ❌ Not Recommended
|
|
|
|
- **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
|
|
|
|
```csharp
|
|
// Just POCOs (Plain Old CLR Objects)
|
|
public record CreateProductCommand
|
|
{
|
|
public string Name { get; init; } = string.Empty;
|
|
public decimal Price { get; init; }
|
|
}
|
|
```
|
|
|
|
### 2. Implement Handlers
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
builder.Services.AddCommand<CreateProductCommand, int, CreateProductCommandHandler>();
|
|
```
|
|
|
|
### 4. Automatic Endpoint Generation
|
|
|
|
Svrnty.CQRS automatically creates HTTP or gRPC endpoints:
|
|
|
|
**HTTP:**
|
|
```
|
|
POST /api/command/createProduct
|
|
```
|
|
|
|
**gRPC:**
|
|
```protobuf
|
|
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](02-installation.md) →**
|
|
|
|
## See Also
|
|
|
|
- [Architecture: CQRS Pattern](../architecture/cqrs-pattern.md) - Deeper dive into the pattern
|
|
- [Architecture: Metadata Discovery](../architecture/metadata-discovery.md) - How discovery works
|
|
- [Best Practices: Command Design](../best-practices/command-design.md) - Designing effective commands
|
|
- [Best Practices: Query Design](../best-practices/query-design.md) - Query optimization patterns
|