323 lines
7.0 KiB
Markdown
323 lines
7.0 KiB
Markdown
# Best Practices
|
|
|
|
Guidelines and patterns for building production-ready applications with Svrnty.CQRS.
|
|
|
|
## Overview
|
|
|
|
This section provides best practices, design patterns, and recommendations for using Svrnty.CQRS effectively in production environments.
|
|
|
|
## Topics
|
|
|
|
### [Command Design](command-design.md)
|
|
|
|
Design effective commands:
|
|
- Single responsibility
|
|
- Validation at boundaries
|
|
- Immutability
|
|
- Rich vs anemic models
|
|
|
|
### [Query Design](query-design.md)
|
|
|
|
Optimize query performance:
|
|
- Projection patterns
|
|
- Database indexing
|
|
- Caching strategies
|
|
- Pagination
|
|
|
|
### [Event Design](event-design.md)
|
|
|
|
Event versioning and schema evolution:
|
|
- Naming conventions
|
|
- Event versioning strategies
|
|
- Backward compatibility
|
|
- Upcasting patterns
|
|
|
|
### [Error Handling](error-handling.md)
|
|
|
|
Handle errors gracefully:
|
|
- Exception types and mapping
|
|
- Validation vs business errors
|
|
- Retry strategies
|
|
- Dead letter queues
|
|
|
|
### [Security](security.md)
|
|
|
|
Secure your application:
|
|
- Authentication
|
|
- Authorization services
|
|
- Input validation
|
|
- Rate limiting
|
|
|
|
### [Performance](performance.md)
|
|
|
|
Optimize for performance:
|
|
- Connection pooling
|
|
- Batch operations
|
|
- Async/await patterns
|
|
- Database tuning
|
|
|
|
### [Testing](testing.md)
|
|
|
|
Test your CQRS application:
|
|
- Unit testing handlers
|
|
- Integration testing
|
|
- Event replay testing
|
|
- Projection testing
|
|
|
|
### [Deployment](deployment.md)
|
|
|
|
Deploy to production:
|
|
- Configuration management
|
|
- Database migrations
|
|
- Health checks
|
|
- Monitoring setup
|
|
|
|
### [Multi-Tenancy](multi-tenancy.md)
|
|
|
|
Multi-tenant patterns:
|
|
- Tenant isolation strategies
|
|
- Database per tenant vs shared
|
|
- Query filtering
|
|
- Security considerations
|
|
|
|
## Quick Reference
|
|
|
|
### Command Best Practices
|
|
|
|
```csharp
|
|
// ✅ Good
|
|
public record CreateOrderCommand
|
|
{
|
|
[Required]
|
|
public int CustomerId { get; init; }
|
|
|
|
[Required, MinLength(1)]
|
|
public List<OrderItem> Items { get; init; } = new();
|
|
|
|
public decimal TotalAmount => Items.Sum(i => i.Price * i.Quantity);
|
|
}
|
|
|
|
// ❌ Bad
|
|
public class CreateOrderCommand
|
|
{
|
|
public int CustomerId; // Not init-only
|
|
public List<OrderItem> Items; // Mutable, no validation
|
|
// Missing calculated properties
|
|
}
|
|
```
|
|
|
|
### Query Best Practices
|
|
|
|
```csharp
|
|
// ✅ Good - Projection with pagination
|
|
public class ListOrdersQuery
|
|
{
|
|
public int Page { get; init; } = 1;
|
|
public int PageSize { get; init; } = 20;
|
|
}
|
|
|
|
public async Task<PagedResult<OrderSummary>> HandleAsync(ListOrdersQuery query)
|
|
{
|
|
return await _context.Orders
|
|
.AsNoTracking()
|
|
.Skip((query.Page - 1) * query.PageSize)
|
|
.Take(query.PageSize)
|
|
.Select(o => new OrderSummary { ... })
|
|
.ToPagedResultAsync();
|
|
}
|
|
|
|
// ❌ Bad - No projection, no pagination
|
|
public async Task<List<Order>> HandleAsync()
|
|
{
|
|
return await _context.Orders.ToListAsync(); // Loads everything!
|
|
}
|
|
```
|
|
|
|
### Event Best Practices
|
|
|
|
```csharp
|
|
// ✅ Good
|
|
public record OrderPlacedEvent
|
|
{
|
|
public string EventId { get; init; } = Guid.NewGuid().ToString();
|
|
public int OrderId { get; init; }
|
|
public DateTimeOffset PlacedAt { get; init; }
|
|
public string CorrelationId { get; init; } = string.Empty;
|
|
public int Version { get; init; } = 1; // Versioning from day 1
|
|
}
|
|
|
|
// ❌ Bad
|
|
public class OrderPlaced // Present tense
|
|
{
|
|
public int OrderId; // Mutable
|
|
// No event ID, timestamp, or version
|
|
}
|
|
```
|
|
|
|
## Architecture Patterns
|
|
|
|
### Modular Solution Structure
|
|
|
|
```
|
|
Solution/
|
|
├── MyApp.Api/ # HTTP/gRPC endpoints
|
|
│ ├── Program.cs
|
|
│ └── appsettings.json
|
|
├── MyApp.CQRS/ # Commands, queries, handlers
|
|
│ ├── Commands/
|
|
│ ├── Queries/
|
|
│ └── Validators/
|
|
├── MyApp.Domain/ # Domain models, events
|
|
│ ├── Entities/
|
|
│ ├── Events/
|
|
│ └── ValueObjects/
|
|
└── MyApp.Infrastructure/ # Data access, external services
|
|
├── Repositories/
|
|
├── DbContext/
|
|
└── ExternalServices/
|
|
```
|
|
|
|
**Benefits:**
|
|
- Clear separation of concerns
|
|
- Testable in isolation
|
|
- Easy to navigate
|
|
- Supports team scaling
|
|
|
|
### CQRS + Event Sourcing
|
|
|
|
```
|
|
Command → Handler → Domain Model → Events → Event Store
|
|
↓
|
|
Projections → Read Models → Queries
|
|
```
|
|
|
|
### Repository Pattern
|
|
|
|
```csharp
|
|
public interface IOrderRepository
|
|
{
|
|
Task<Order> GetByIdAsync(int id);
|
|
Task AddAsync(Order order);
|
|
Task UpdateAsync(Order order);
|
|
}
|
|
|
|
// Use in command handlers
|
|
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, int>
|
|
{
|
|
private readonly IOrderRepository _repository;
|
|
|
|
public async Task<int> HandleAsync(CreateOrderCommand command, CancellationToken ct)
|
|
{
|
|
var order = Order.Create(command);
|
|
await _repository.AddAsync(order);
|
|
return order.Id;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Common Anti-Patterns
|
|
|
|
### ❌ Anemic Domain Models
|
|
|
|
```csharp
|
|
// Bad - Logic in handler, not domain
|
|
public class Order
|
|
{
|
|
public int Id { get; set; }
|
|
public decimal Total { get; set; }
|
|
public string Status { get; set; }
|
|
}
|
|
|
|
public class PlaceOrderHandler
|
|
{
|
|
public async Task HandleAsync(PlaceOrderCommand cmd)
|
|
{
|
|
var order = new Order
|
|
{
|
|
Total = cmd.Items.Sum(i => i.Price * i.Quantity),
|
|
Status = "Placed"
|
|
};
|
|
// All logic in handler!
|
|
}
|
|
}
|
|
```
|
|
|
|
**Better:**
|
|
```csharp
|
|
// Good - Rich domain model
|
|
public class Order
|
|
{
|
|
private readonly List<OrderItem> _items = new();
|
|
|
|
public static Order Create(List<OrderItem> items)
|
|
{
|
|
if (items.Count == 0)
|
|
throw new InvalidOperationException("Order must have items");
|
|
|
|
var order = new Order();
|
|
order._items.AddRange(items);
|
|
return order;
|
|
}
|
|
|
|
public decimal Total => _items.Sum(i => i.Price * i.Quantity);
|
|
}
|
|
```
|
|
|
|
### ❌ Querying Write Model
|
|
|
|
```csharp
|
|
// Bad - Querying entity directly
|
|
public async Task<Order> GetOrderAsync(int id)
|
|
{
|
|
return await _context.Orders
|
|
.Include(o => o.Items)
|
|
.Include(o => o.Customer)
|
|
.FirstOrDefaultAsync(o => o.Id == id);
|
|
}
|
|
```
|
|
|
|
**Better:**
|
|
```csharp
|
|
// Good - Use projection/read model
|
|
public async Task<OrderSummary> GetOrderAsync(int id)
|
|
{
|
|
return await _readContext.OrderSummaries
|
|
.FirstOrDefaultAsync(o => o.OrderId == id);
|
|
}
|
|
```
|
|
|
|
### ❌ Missing Validation
|
|
|
|
```csharp
|
|
// Bad - No validation
|
|
public class CreateOrderHandler
|
|
{
|
|
public async Task<int> HandleAsync(CreateOrderCommand cmd)
|
|
{
|
|
var order = new Order { CustomerId = cmd.CustomerId }; // What if CustomerId is invalid?
|
|
await _repository.AddAsync(order);
|
|
return order.Id;
|
|
}
|
|
}
|
|
```
|
|
|
|
**Better:**
|
|
```csharp
|
|
// Good - FluentValidation
|
|
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
|
|
{
|
|
public CreateOrderCommandValidator()
|
|
{
|
|
RuleFor(x => x.CustomerId).GreaterThan(0);
|
|
RuleFor(x => x.Items).NotEmpty();
|
|
}
|
|
}
|
|
```
|
|
|
|
## See Also
|
|
|
|
- [Architecture Overview](../architecture/README.md)
|
|
- [Event Streaming](../event-streaming/README.md)
|
|
- [Observability](../observability/README.md)
|
|
- [Troubleshooting](../troubleshooting/README.md)
|