dotnet-cqrs/docs/best-practices
2025-12-11 01:18:24 -05:00
..
command-design.md this is a mess 2025-12-11 01:18:24 -05:00
deployment.md this is a mess 2025-12-11 01:18:24 -05:00
error-handling.md this is a mess 2025-12-11 01:18:24 -05:00
event-design.md this is a mess 2025-12-11 01:18:24 -05:00
multi-tenancy.md this is a mess 2025-12-11 01:18:24 -05:00
performance.md this is a mess 2025-12-11 01:18:24 -05:00
query-design.md this is a mess 2025-12-11 01:18:24 -05:00
README.md this is a mess 2025-12-11 01:18:24 -05:00
security.md this is a mess 2025-12-11 01:18:24 -05:00
testing.md this is a mess 2025-12-11 01:18:24 -05:00

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

Design effective commands:

  • Single responsibility
  • Validation at boundaries
  • Immutability
  • Rich vs anemic models

Query Design

Optimize query performance:

  • Projection patterns
  • Database indexing
  • Caching strategies
  • Pagination

Event Design

Event versioning and schema evolution:

  • Naming conventions
  • Event versioning strategies
  • Backward compatibility
  • Upcasting patterns

Error Handling

Handle errors gracefully:

  • Exception types and mapping
  • Validation vs business errors
  • Retry strategies
  • Dead letter queues

Security

Secure your application:

  • Authentication
  • Authorization services
  • Input validation
  • Rate limiting

Performance

Optimize for performance:

  • Connection pooling
  • Batch operations
  • Async/await patterns
  • Database tuning

Testing

Test your CQRS application:

  • Unit testing handlers
  • Integration testing
  • Event replay testing
  • Projection testing

Deployment

Deploy to production:

  • Configuration management
  • Database migrations
  • Health checks
  • Monitoring setup

Multi-Tenancy

Multi-tenant patterns:

  • Tenant isolation strategies
  • Database per tenant vs shared
  • Query filtering
  • Security considerations

Quick Reference

Command Best Practices

// ✅ 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

// ✅ 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

// ✅ 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

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

// 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:

// 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

// 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:

// Good - Use projection/read model
public async Task<OrderSummary> GetOrderAsync(int id)
{
    return await _readContext.OrderSummaries
        .FirstOrDefaultAsync(o => o.OrderId == id);
}

Missing Validation

// 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:

// Good - FluentValidation
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId).GreaterThan(0);
        RuleFor(x => x.Items).NotEmpty();
    }
}

See Also