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

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