CQRSlite Documentation

repository·master·Indexed 22 days ago

https://github.com/gautema/cqrslite

A lightweight CQRS and Event Sourcing framework for .NET. It provides essential building blocks including ICommand, IEvent, and IQuery interfaces, along with support for AggregateRoot, snapshotting via SnapshotAggregateRoot, and the Unit of Work pattern through ISession. Users must provide their own implementation of the IEventStore interface for persistence.

Tokens
24.7K
Snippets
62
Records
79
Agent score
74%

What's inside CQRSlite

  1. Query Data Across Aggregates

    master

    Because aggregates are isolated, you should not query them directly for complex data requirements. Instead, use read models (projections):

    1. Update: Event handlers listen to events and update denormalized read models.
    2. Read: Query handlers read from these optimized, denormalized models.
    3. Join: Read models allow you to join data across multiple aggregates into a single view.
  2. Implement an EventStore

    master

    CQRSlite is a framework and does not provide a built-in EventStore. You must implement your own EventStore to handle persistence. Your implementation should consider your specific requirements for:

    • Database type (SQL vs NoSQL)
    • Deployment (Cloud vs on-premises)
    • Performance and transaction guarantees
    • Event schema evolution strategies
  3. Understand the CQRSlite Architecture

    master

    CQRSlite is a lightweight CQRS (Command Query Responsibility Segregation) and Event Sourcing framework for .NET. It separates the application into a Write Side (Commands) and a Read Side (Queries).

    Write Side (Commands)

    • ICommandSender: Entry point for sending commands.
    • Command Handlers: Process commands and interact with the domain.
    • ISession (Unit of Work): Tracks changes to aggregates.
    • IRepository: Persists aggregates.
    • Aggregate Roots: Maintain domain consistency and emit events.
    • IEventStore: Persists and retrieves events.

    Read Side (Queries)

    • IQueryProcessor: Entry point for processing queries.
    • Query Handlers: Retrieve data from read models or DTOs.
    • Event Handlers: Listen to events to update read models.

    Core Design Principles

    • Minimal Dependencies: Only depends on Microsoft.Extensions.Caching.Memory and Microsoft.CSharp.
    • Pluggability: Every component is replaceable with custom implementations.
    • Convention over Configuration: Uses convention-based routing (e.g., for event application).
    • Target Frameworks: netstandard2.0 and net9.0.
  4. Manage Cross-Aggregate Coordination

    master

    A single command should only modify one aggregate. To coordinate changes across multiple aggregates, do not attempt to modify them in one command. Instead, use one of the following patterns:

    • Sagas or Process Managers: To manage long-running or multi-step workflows.
    • Event-Driven Triggers: Emit an event from the first aggregate, and use an event handler to trigger a new command for the subsequent aggregates.
  5. Best practices for Command Handlers and Read Models

    master

    Command Handlers

    • One purpose: Each command should perform a single logical action.
    • Validation split: Perform technical validation (permissions, dependencies) in the handler, but perform business rule validation inside the aggregate.
    • Use ISession: Always use ISession to load aggregates and commit changes to ensure consistency.

    Read Models

    • Denormalized: Optimize read models for specific query patterns rather than strict normalization.
    • Eventually consistent: Accept that read models may lag slightly behind the write model.
    • Multiple models: Create different read models for different use cases.
    • Cache: Use caching for expensive or frequently accessed queries.
  6. Best practices for Aggregate and Event design

    master

    Aggregate Design

    • Keep aggregates small: Large aggregates cause performance issues during event replay.
    • One aggregate per transaction: Avoid modifying multiple aggregates within a single command handler.
    • Protect invariants: All business rules must be enforced inside the aggregate.
    • Use meaningful events: Capture business intent (e.g., OrderPlaced) rather than generic state changes (e.g., OrderStateChanged).

    Event Design

    • Events are immutable: Never change an event structure once it has been deployed.
    • Events are facts: Use past tense and describe what happened.
    • Include all necessary data: Events should be self-contained so they can be replayed without external lookups.
    • Consider versioning: Plan for how event schemas will evolve over time.
  7. Handle Optimistic Concurrency

    master

    CQRSlite supports optimistic concurrency checking. To use it:

    1. Include an ExpectedVersion property in your command.
    2. When retrieving the aggregate via ISession.Get<T>(id, expectedVersion), CQRSlite will throw a ConcurrencyException if the version in the store does not match the expectedVersion provided.
    public class ChangeProductPrice : ICommand
    {
        public Guid Id { get; set; }
        public decimal NewPrice { get; set; }
        public int ExpectedVersion { get; set; } // Important!
    }
    
    public class ProductCommandHandler : ICommandHandler<ChangeProductPrice>
    {
        private readonly ISession _session;
    
        public ProductCommandHandler(ISession session)
        {
            _session = session;
        }
    
        public async Task Handle(ChangeProductPrice message)
        {
            // Pass expected version - throws ConcurrencyException if mismatch
            var product = await _session.Get<Product>(message.Id, message.ExpectedVersion);
            product.ChangePrice(message.NewPrice);
            await _session.Commit();
        }
    }
  8. Define Messages: Commands, Events, and Queries

    master

    CQRSlite uses specific interfaces to categorize messages:

    • Commands (ICommand): Represent an intent to change state. They can include concurrency control by including an ExpectedVersion property.
    • Events (IEvent): Represent something that has happened in the past. They typically include metadata like Version and TimeStamp alongside business data.
    • Queries (IQuery<T>): Represent a request for data. The generic type T defines the expected return type (often a DTO).
    // Command
    public class CreateProduct : ICommand
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
    
    // Command with Concurrency
    public class UpdateProductPrice : ICommand
    {
        public Guid Id { get; set; }
        public decimal NewPrice { get; set; }
        public int ExpectedVersion { get; set; }
    }
    
    // Event
    public class ProductCreated : IEvent
    {
        public Guid Id { get; set; }
        public int Version { get; set; }
        public DateTimeOffset TimeStamp { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
    
    // Query
    public class GetProduct : IQuery<ProductDto>
    {
        public Guid Id { get; set; }
    }
  9. How the Event Application Pattern works

    master

    CQRSlite uses convention-based routing to apply events to aggregates. This allows the domain model to react to its own history.

    The Lifecycle:

    1. The domain logic calls ApplyChange(event).
    2. The framework calls the virtual ApplyEvent(event) method.
    3. ApplyEvent uses reflection to locate a private method matching the signature Apply(EventType e).
    4. Performance: To avoid the overhead of reflection, these method invocations are cached using DynamicInvoker (compiled expressions), making subsequent applications nearly as fast as direct method calls.
  10. Implement an Aggregate Root

    master

    Aggregates manage state and enforce business rules. To implement one:

    1. Inherit from AggregateRoot.
    2. Use ApplyChange(event) to record new state changes.
    3. Implement Apply(event) private methods to update the internal state when an event is applied (this is used during rehydration).
    4. Provide a private parameterless constructor for rehydration purposes.
    public class Product : AggregateRoot
    {
        private string _name;
        private decimal _price;
        private bool _discontinued;
    
        public Product(Guid id, string name, decimal price)
        {
            Id = id;
            ApplyChange(new ProductCreated(id, name, price));
        }
    
        private Product() { } // For rehydration
    
        public void ChangePrice(decimal newPrice)
        {
            if (_discontinued)
                throw new InvalidOperationException("Cannot change price of discontinued product");
    
            ApplyChange(new ProductPriceChanged(Id, newPrice));
        }
    
        private void Apply(ProductCreated e)
        {
            _name = e.Name;
            _price = e.Price;
        }
    
        private void Apply(ProductPriceChanged e)
        {
            _price = e.NewPrice;
        }
    }
  11. Implement the IEventStore interface

    master

    CQRSlite provides the framework for CQRS/ES but does not include a default event store implementation. You are responsible for implementing IEventStore to handle persistence.

    Common storage options include:

    • SQL databases (SQL Server, PostgreSQL, MySQL)
    • NoSQL databases (MongoDB, CosmosDB)
    • Specialized event stores (Event Store DB)
    • Cloud storage (Azure Table Storage)
    • In-memory storage (useful for testing)
  12. Perform Multiple Operations using ISession

    master

    When an operation involves multiple aggregates, use ISession to manage the unit of work. Retrieve aggregates via _session.Get<T>(id), perform operations on them, and then call _session.Commit() to persist all changes atomically.

    public async Task Handle(TransferInventory command)
    {
        // Get both aggregates in the same session
        var source = await _session.Get<InventoryItem>(command.SourceId);
        var destination = await _session.Get<InventoryItem>(command.DestinationId);
    
        // Perform operations
        source.Remove(command.Quantity);
        destination.Add(command.Quantity);
    
        // Commit saves both aggregates
        await _session.Commit();
    }