### SqlSnapshotStore Save and Get Methods Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of Save and Get methods for SqlSnapshotStore, handling snapshot persistence using JSON serialization. ```csharp public class SqlSnapshotStore : ISnapshotStore { public async Task Save(Snapshot snapshot, CancellationToken cancellationToken) { var json = JsonSerializer.Serialize(snapshot, snapshot.GetType()); await _connection.ExecuteAsync( "INSERT INTO Snapshots (Id, Version, Type, Data) VALUES (@Id, @Version, @Type, @Data) " + "ON CONFLICT (Id) DO UPDATE SET Version = @Version, Data = @Data", new { snapshot.Id, snapshot.Version, Type = snapshot.GetType().AssemblyQualifiedName, Data = json }); } public async Task Get(Guid id, CancellationToken cancellationToken) { var record = await _connection.QueryFirstOrDefaultAsync( "SELECT * FROM Snapshots WHERE Id = @Id", new { Id = id }); if (record == null) return null; var type = Type.GetType(record.Type); return (Snapshot)JsonSerializer.Deserialize(record.Data, type); } } ``` -------------------------------- ### Installation Source: https://github.com/gautema/cqrslite/blob/master/README.md Install the CQRSlite NuGet package. ```bash dotnet add package CQRSlite ``` -------------------------------- ### Repository Usage Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example showing how to register the Repository with dependency injection. ```csharp services.AddScoped(sp => new Repository(sp.GetService())); ``` -------------------------------- ### Queries Example Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of defining a query and its handler. ```csharp public class GetInventoryItemDetails : IQuery { public Guid Id { get; set; } } public class InventoryItemDetailsHandler : IQueryHandler { public Task Handle(GetInventoryItemDetails query) { return Task.FromResult(_database[query.Id]); } } ``` -------------------------------- ### SqlEventStore Get Method Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of the Get method for SqlEventStore, retrieving events for an aggregate. ```csharp public async Task> Get(Guid aggregateId, int fromVersion, CancellationToken cancellationToken) { var eventRecords = await _connection.QueryAsync( "SELECT * FROM Events WHERE AggregateId = @Id AND Version > @Version ORDER BY Version", new { Id = aggregateId, Version = fromVersion }); return eventRecords.Select(DeserializeEvent); } ``` -------------------------------- ### IQuery Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of an IQuery and its DTO. ```csharp public class GetInventoryItemDetails : IQuery { public Guid Id { get; set; } } public class InventoryItemDetailsDto { public Guid Id { get; set; } public string Name { get; set; } public int CurrentCount { get; set; } public int Version { get; set; } } ``` -------------------------------- ### Session Usage Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example showing how to register the Session with dependency injection. ```csharp services.AddScoped(); ``` -------------------------------- ### Minimal Dependency Injection Setup Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Basic dependency injection setup for cqrslite services. ```csharp public void ConfigureServices(IServiceCollection services) { // Router (central hub) var router = new Router(); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); // Event store (you must implement) services.AddSingleton(); // Repository services.AddScoped(sp => new Repository(sp.GetService())); // Session services.AddScoped(); // Register handlers var serviceProvider = services.BuildServiceProvider(); var registrar = new RouteRegistrar(serviceProvider); registrar.Register(typeof(ProductCommandHandlers).Assembly); } ``` -------------------------------- ### SQL Event Store Example Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md An example implementation of an IEventStore using SQL to persist events. ```csharp public class SqlEventStore : IEventStore { private readonly IDbConnection _connection; private readonly IEventPublisher _publisher; public SqlEventStore(IDbConnection connection, IEventPublisher publisher) { _connection = connection; _publisher = publisher; } public async Task Save(IEnumerable events, CancellationToken cancellationToken = default) { using var transaction = _connection.BeginTransaction(); try { foreach (var @event in events) { // Serialize and save await _connection.ExecuteAsync( "INSERT INTO Events (AggregateId, Version, Type, Data, Timestamp) " + "VALUES (@Id, @Version, @Type, @Data, @TimeStamp)", new { @event.Id, @event.Version, Type = @event.GetType().AssemblyQualifiedName, Data = JsonSerializer.Serialize(@event, @event.GetType()), @event.TimeStamp }, transaction); // Publish after save await _publisher.Publish(@event, cancellationToken); } transaction.Commit(); } catch { transaction.Rollback(); throw; } } public async Task> Get(Guid aggregateId, int fromVersion, CancellationToken cancellationToken = default) { var records = await _connection.QueryAsync( "SELECT * FROM Events WHERE AggregateId = @AggregateId AND Version > @FromVersion ORDER BY Version", new { AggregateId = aggregateId, FromVersion = fromVersion }); return records.Select(r => { var type = Type.GetType(r.Type); return (IEvent)JsonSerializer.Deserialize(r.Data, type); }); } } ``` -------------------------------- ### ICommandHandler Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of an ICommandHandler. ```csharp public class InventoryCommandHandlers : ICommandHandler { private readonly ISession _session; public InventoryCommandHandlers(ISession session) { _session = session; } public async Task Handle(CreateInventoryItem message) { var item = new InventoryItem(message.Id, message.Name); await _session.Add(item); await _session.Commit(); } } ``` -------------------------------- ### Aggregate Example Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of an AggregateRoot implementation for InventoryItem. ```csharp public class InventoryItem : AggregateRoot { private bool _activated; private string _name; // Constructor for new aggregates public InventoryItem(Guid id, string name) { Id = id; ApplyChange(new InventoryItemCreated(id, name)); } // Required parameterless constructor for rehydration private InventoryItem() { } // Business logic methods public void ChangeName(string newName) { if (!_activated) throw new InvalidOperationException("Item is deactivated"); ApplyChange(new InventoryItemRenamed(Id, newName)); } public void Deactivate() { if (!_activated) throw new InvalidOperationException("Already deactivated"); ApplyChange(new InventoryItemDeactivated(Id)); } // Convention-based event application (private methods) private void Apply(InventoryItemCreated e) { _name = e.Name; _activated = true; } private void Apply(InventoryItemRenamed e) { _name = e.NewName; } private void Apply(InventoryItemDeactivated e) { _activated = false; } } ``` -------------------------------- ### Implement Your Aggregate Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example implementation of a Product aggregate. ```csharp 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; } } ``` -------------------------------- ### CustomSnapshotStrategy Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of a custom snapshot strategy. ```csharp public class CustomSnapshotStrategy : ISnapshotStrategy { public bool IsSnapshotable(Type aggregateType) { return typeof(ISnapshotAggregate<>).IsAssignableFrom(aggregateType); } public bool ShouldMakeSnapShot(AggregateRoot aggregate) { // Snapshot after 50 events, then every 100 if (aggregate.Version == 50) return true; return aggregate.Version >= 100 && aggregate.Version % 100 == 0; } } ``` -------------------------------- ### Dependency Injection Setup with Caching Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Dependency injection setup for cqrslite services with caching enabled. ```csharp public void ConfigureServices(IServiceCollection services) { // Router var router = new Router(); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); // Event store services.AddSingleton(); // Cache services.AddSingleton(); // Repository with caching services.AddScoped(sp => new CacheRepository( new Repository(sp.GetService()), sp.GetService(), sp.GetService())); // Session services.AddScoped(); // Register handlers var serviceProvider = services.BuildServiceProvider(); var registrar = new RouteRegistrar(serviceProvider); registrar.Register(typeof(ProductCommandHandlers).Assembly); } ``` -------------------------------- ### IEvent Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of an IEvent. ```csharp public class InventoryItemCreated : IEvent { public Guid Id { get; set; } public int Version { get; set; } public DateTimeOffset TimeStamp { get; set; } public string Name { get; set; } public int InitialCount { get; set; } } ``` -------------------------------- ### Example of IQueryHandler Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of a query handler for GetInventoryItemDetails. ```csharp public class InventoryItemDetailView : IQueryHandler { private readonly IReadDatabase _database; public async Task Handle(GetInventoryItemDetails message) { return await _database.GetById(message.Id); } } ``` -------------------------------- ### Query Handler Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of query handlers for retrieving product information. ```csharp public class ProductQueryHandlers : IQueryHandler, IQueryHandler> { private readonly IReadDatabase _database; public ProductQueryHandlers(IReadDatabase database) { _database = database; } public async Task Handle(GetProduct query) { return await _database.GetById(query.Id); } public async Task> Handle(GetAllProducts query) { return await _database.GetAll(); } } ``` -------------------------------- ### IRepository.Get Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example demonstrating how to retrieve an aggregate using the IRepository interface. ```csharp var product = await _repository.Get(productId); ``` -------------------------------- ### Command Handlers - Example Implementation Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of a command handler demonstrating technical validation, aggregate loading, business logic, and committing changes. ```csharp public async Task Handle(PlaceOrder command) { // Technical validation if (!await _permissions.CanPlaceOrder(command.CustomerId)) throw new UnauthorizedAccessException(); // Load aggregate var customer = await _session.Get(command.CustomerId); // Business logic (validation happens in aggregate) var order = customer.PlaceOrder(command.Lines); // Track and commit await _session.Add(order); await _session.Commit(); } ``` -------------------------------- ### ISession.Get Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example showing how to retrieve an aggregate from the session, modify it, and commit the changes. ```csharp var product = await _session.Get(productId, expectedVersion: 5); product.ChangePrice(150m); await _session.Commit(); ``` -------------------------------- ### Command Example: CreateInventoryItem Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md An example of a command class that inherits from ICommand, used to initiate a state change. ```csharp public class CreateInventoryItem : ICommand { public Guid Id { get; set; } public string Name { get; set; } } ``` -------------------------------- ### ISession.Add Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example demonstrating how to add a new aggregate to the session and commit the changes. ```csharp var product = new Product(Guid.NewGuid(), "New Product", 99.99m); await _session.Add(product); await _session.Commit(); ``` -------------------------------- ### ConcurrencyException Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of how to catch and handle a ConcurrencyException. ```csharp try { await _repository.Save(product, expectedVersion: 5); } catch (ConcurrencyException ex) { // Handle conflict - retry, merge, or inform user Console.WriteLine($"Concurrency conflict: Expected v{ex.ExpectedVersion}, was v{ex.ActualVersion}"); } ``` -------------------------------- ### Setup with Snapshotting and Caching Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Configures services for CQRSlite, including the router, event store, snapshot support, cache, repository with decorators, and session. It also registers command handlers. ```csharp public void ConfigureServices(IServiceCollection services) { // Router var router = new Router(); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); // Event store services.AddSingleton(); // Snapshot support services.AddSingleton(); services.AddSingleton(); // Cache services.AddSingleton(); // Repository with all decorators services.AddScoped(sp => new CacheRepository( new SnapshotRepository( sp.GetService(), sp.GetService(), new Repository(sp.GetService()), sp.GetService()), sp.GetService(), sp.GetService())); // Session services.AddScoped(); // Register handlers var serviceProvider = services.BuildServiceProvider(); var registrar = new RouteRegistrar(serviceProvider); registrar.Register(typeof(ProductCommandHandlers).Assembly); } ``` -------------------------------- ### ICancellableCommandHandler Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of an ICancellableCommandHandler. ```csharp public class InventoryCommandHandlers : ICancellableCommandHandler { public async Task Handle(CreateInventoryItem message, CancellationToken token) { var item = new InventoryItem(message.Id, message.Name); await _session.Add(item, token); await _session.Commit(token); } } ``` -------------------------------- ### Example of IEventHandler Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of an event handler for InventoryItemCreated. ```csharp public class InventoryItemDetailView : IEventHandler { private readonly IReadDatabase _database; public async Task Handle(InventoryItemCreated message) { await _database.Insert(new InventoryItemDetailsDto { Id = message.Id, Name = message.Name, CurrentCount = 0, Version = message.Version }); } } ``` -------------------------------- ### Handling Optimistic Concurrency Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of using optimistic concurrency checking with CQRSlite by including the ExpectedVersion in commands and passing it to the session's Get method. ```csharp public class ChangeProductPrice : ICommand { public Guid Id { get; set; } public decimal NewPrice { get; set; } public int ExpectedVersion { get; set; } // Important! } public class ProductCommandHandler : ICommandHandler { private readonly ISession _session; public async Task Handle(ChangeProductPrice message) { // Pass expected version - throws ConcurrencyException if mismatch var product = await _session.Get(message.Id, message.ExpectedVersion); product.ChangePrice(message.NewPrice); await _session.Commit(); } } ``` ```html ``` -------------------------------- ### IRepository.Save Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example demonstrating how to save an aggregate using the IRepository interface. ```csharp public async Task Handle(ChangeProductPrice command) { var product = await _repository.Get(command.Id); product.ChangePrice(command.NewPrice); await _repository.Save(product, command.ExpectedVersion); } ``` -------------------------------- ### AggregateNotFoundException Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of how to catch and handle an AggregateNotFoundException. ```csharp try { var product = await _repository.Get(productId); } catch (AggregateNotFoundException ex) { return NotFound($"Product {ex.Id} not found"); } ``` -------------------------------- ### Example Snapshot Implementation Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md An example of a concrete snapshot class inheriting from the base Snapshot class. ```csharp public class ProductSnapshot : Snapshot { public string Name { get; set; } public decimal Price { get; set; } public bool Discontinued { get; set; } } ``` -------------------------------- ### Testing Command Handlers Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of a unit test for a command handler, verifying that a new product is added to the session. ```csharp [Fact] public async Task CreateProduct_AddsProductToSession() { // Arrange var mockSession = new Mock(); var handler = new ProductCommandHandlers(mockSession.Object); var command = new CreateProduct { Id = Guid.NewGuid(), Name = "Test", Price = 100m }; // Act await handler.Handle(command); // Assert mockSession.Verify(s => s.Add(It.IsAny()), Times.Once); mockSession.Verify(s => s.Commit(), Times.Once); } ``` -------------------------------- ### Integration Testing Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Shows an example of integration testing the full flow of creating and retrieving a product using a WebApplicationFactory. ```csharp public class ProductIntegrationTests : IClassFixture> { private readonly WebApplicationFactory _factory; public ProductIntegrationTests(WebApplicationFactory factory) { _factory = factory; } [Fact] public async Task CreateAndRetrieveProduct_WorksEndToEnd() { var client = _factory.CreateClient(); var productId = Guid.NewGuid(); // Create product var createCommand = new CreateProduct { Id = productId, Name = "Test Product", Price = 99.99m }; var createResponse = await client.PostAsJsonAsync("/api/products", createCommand); Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode); // Wait for eventual consistency await Task.Delay(100); // Retrieve product var getResponse = await client.GetAsync($"/api/products/{productId}"); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); var product = await getResponse.Content.ReadFromJsonAsync(); Assert.Equal("Test Product", product.Name); Assert.Equal(99.99m, product.Price); } } ``` -------------------------------- ### CreateSnapshot Method Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of the CreateSnapshot method for a snapshotting aggregate. ```csharp public class Product : SnapshotAggregateRoot { private string _name; private decimal _price; private bool _discontinued; protected override ProductSnapshot CreateSnapshot() { return new ProductSnapshot { Id = Id, Version = Version, Name = _name, Price = _price, Discontinued = _discontinued }; } } ``` -------------------------------- ### RestoreFromSnapshot Method Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example implementation of the RestoreFromSnapshot method for a snapshotting aggregate. ```csharp protected override void RestoreFromSnapshot(ProductSnapshot snapshot) { _name = snapshot.Name; _price = snapshot.Price; _discontinued = snapshot.Discontinued; } ``` -------------------------------- ### Processing a Query Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md An example of an API controller that processes a query using the IQueryProcessor interface and handles KeyNotFoundException. ```csharp [HttpGet("{id}")] public async Task> Get(Guid id) { try { var result = await _queryProcessor.Query(new GetProduct { Id = id }); return Ok(result); } catch (KeyNotFoundException) { return NotFound(); } } ``` -------------------------------- ### Sending a Command Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md An example of an API controller that sends a command using the ICommandSender interface. ```csharp [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ICommandSender _commandSender; [HttpPost] public async Task Create([FromBody] CreateProduct command, CancellationToken token) { await _commandSender.Send(command, token); return CreatedAtAction(nameof(Get), new { id = command.Id }, null); } } ``` -------------------------------- ### Command Handler Example Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md An example of a command handler that implements ICommandHandler, responsible for processing a specific command. ```csharp public class InventoryCommandHandler : ICommandHandler { private readonly ISession _session; public InventoryCommandHandler(ISession session) { _session = session; } public async Task Handle(CreateInventoryItem message) { var item = new InventoryItem(message.Id, message.Name); await _session.Add(item); await _session.Commit(); } } ``` -------------------------------- ### Define Your Messages Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example definitions for Commands, Events, and Queries. ```csharp // Commands public class CreateProduct : ICommand { public Guid Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } // Events 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; } } // Queries public class GetProduct : IQuery { public Guid Id { get; set; } } ``` -------------------------------- ### ISession.Commit Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example illustrating how to commit changes for multiple aggregates within a single session. ```csharp var product = await _session.Get(productId); product.ChangePrice(150m); var category = await _session.Get(categoryId); category.AddProduct(productId); // Saves both aggregates await _session.Commit(); ``` -------------------------------- ### ApplyEvent Method - Convention-based Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of a convention-based ApplyEvent method in an aggregate. ```csharp public class Product : AggregateRoot { private string _name; private decimal _price; // Convention-based event application private void Apply(ProductCreated e) { _name = e.Name; _price = e.Price; } private void Apply(ProductPriceChanged e) { _price = e.NewPrice; } } ``` -------------------------------- ### Command, Event, and Query Handlers Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Examples of implementing command, event, and query handlers using cqrslite interfaces. ```csharp // Command Handler public class ProductCommandHandlers : ICommandHandler { private readonly ISession _session; public ProductCommandHandlers(ISession session) { _session = session; } public async Task Handle(CreateProduct message) { var product = new Product(message.Id, message.Name, message.Price); await _session.Add(product); await _session.Commit(); } } // Event Handler (for read model) public class ProductEventHandlers : ICancellableEventHandler { private readonly IReadDatabase _database; public async Task Handle(ProductCreated message, CancellationToken token) { await _database.Insert(new ProductDto { Id = message.Id, Name = message.Name, Price = message.Price }); } } // Query Handler public class ProductQueryHandlers : IQueryHandler { private readonly IReadDatabase _database; public Task Handle(GetProduct query) { return _database.GetById(query.Id); } } ``` -------------------------------- ### Handling Aggregate Not Found Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of how to handle an AggregateNotFoundException when retrieving a resource. ```csharp public async Task GetProduct(Guid id) { try { var result = await _queryProcessor.Query(new GetProduct { Id = id }); return Ok(result); } catch (AggregateNotFoundException ex) { return NotFound(new { message = $ ``` ```csharp Product {ex.Id} not found ``` -------------------------------- ### Event Example: InventoryItemCreated Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md An example of an event class that inherits from IEvent, representing a fact that has occurred. ```csharp public class InventoryItemCreated : IEvent { public Guid Id { get; set; } public int Version { get; set; } public DateTimeOffset TimeStamp { get; set; } public string Name { get; set; } } ``` -------------------------------- ### Router Usage Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Usage example for Router. ```csharp var router = new Router(); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); services.AddSingleton(router); ``` -------------------------------- ### Usage of IQueryProcessor Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of using IQueryProcessor in a controller to process a query. ```csharp public class ProductController { private readonly IQueryProcessor _queryProcessor; [HttpGet("{id}")] public async Task> Get(Guid id) { var result = await _queryProcessor.Query(new GetProduct { Id = id }); return Ok(result); } } ``` -------------------------------- ### Handling Concurrency Exceptions Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of how to handle concurrency exceptions when updating a resource. ```csharp public async Task UpdateProduct(UpdateProduct command) { try { await _commandSender.Send(command); return Ok(); } catch (ConcurrencyException ex) { return Conflict(new { message = "Resource was modified by another user", resourceId = ex.Id, expectedVersion = ex.ExpectedVersion, actualVersion = ex.ActualVersion }); } } ``` -------------------------------- ### Event Handler Example Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md An example of an event handler that implements ICancellableEventHandler, responsible for reacting to a specific event. ```csharp public class InventoryItemDetailView : ICancellableEventHandler { public Task Handle(InventoryItemCreated message, CancellationToken token) { // Update read model _database.Add(message.Id, new InventoryItemDto { Id = message.Id, Name = message.Name }); return Task.CompletedTask; } } ``` -------------------------------- ### Testing Aggregates Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Examples of unit tests for aggregate behavior, including changing prices and handling discontinued products. ```csharp [Fact] public void ChangingPrice_RaisesCorrectEvent() { // Arrange var product = new Product(Guid.NewGuid(), "Test", 100m); product.FlushUncommittedChanges(); // Clear creation event // Act product.ChangePrice(150m); // Assert var events = product.FlushUncommittedChanges(); Assert.Single(events); var priceChangedEvent = Assert.IsType(events.First()); Assert.Equal(150m, priceChangedEvent.NewPrice); } [Fact] public void ChangingPriceOfDiscontinuedProduct_ThrowsException() { // Arrange var product = new Product(Guid.NewGuid(), "Test", 100m); product.Discontinue(); // Act & Assert Assert.Throws(() => product.ChangePrice(150m)); } ``` -------------------------------- ### SnapshotRepository Usage Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Usage example for SnapshotRepository. ```csharp services.AddScoped(sp => new SnapshotRepository( sp.GetService(), sp.GetService(), new Repository(sp.GetService()), sp.GetService())); ``` -------------------------------- ### ApplyEvent Method - Custom Override Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of overriding the ApplyEvent method for custom event handling. ```csharp protected override void ApplyEvent(IEvent @event) { switch (@event) { case ProductCreated e: _name = e.Name; _price = e.Price; break; case ProductPriceChanged e: _price = e.NewPrice; break; default: base.ApplyEvent(@event); // Fall back to convention break; } } ``` -------------------------------- ### CacheRepository Usage Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Usage example for CacheRepository. ```csharp services.AddSingleton(); services.AddScoped(sp => new CacheRepository( new Repository(sp.GetService()), sp.GetService(), sp.GetService())); ``` -------------------------------- ### Event Handler (Read Model) Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of an event handler that updates a read model based on product events. ```csharp public class ProductListView : ICancellableEventHandler, ICancellableEventHandler, ICancellableEventHandler { private readonly IReadDatabase _database; public ProductListView(IReadDatabase database) { _database = database; } public async Task Handle(ProductCreated e, CancellationToken token) { await _database.Insert(new ProductListItemDto { Id = e.Id, Name = e.Name, Price = e.Price, Version = e.Version }, token); } public async Task Handle(ProductPriceChanged e, CancellationToken token) { var item = await _database.Get(e.Id, token); item.Price = e.NewPrice; item.Version = e.Version; await _database.Update(item, token); } public async Task Handle(ProductDiscontinued e, CancellationToken token) { await _database.Delete(e.Id, token); } } ``` -------------------------------- ### Custom Snapshot Strategy Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example implementation of a custom snapshot strategy by inheriting from ISnapshotStrategy. ```csharp public class CustomSnapshotStrategy : ISnapshotStrategy { public bool IsSnapshotable(Type aggregateType) { // Only snapshot specific aggregate types return aggregateType.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISnapshotAggregate<>)); } public bool ShouldMakeSnapShot(AggregateRoot aggregate) { // Snapshot based on custom logic // Example: Snapshot every 50 events, or after specific event types return aggregate.Version % 50 == 0; } } ``` -------------------------------- ### SqlEventStore Save Method Example Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Best practice implementation of the Save method for SqlEventStore, persisting events and publishing them. ```csharp public class SqlEventStore : IEventStore { private readonly IEventPublisher _publisher; private readonly IDbConnection _connection; public async Task Save(IEnumerable events, CancellationToken cancellationToken) { using var transaction = _connection.BeginTransaction(); try { foreach (var @event in events) { // Save event to database await SaveEventToDatabase(@event); // Publish after save await _publisher.Publish(@event, cancellationToken); } transaction.Commit(); } catch { transaction.Rollback(); throw; } } } ``` -------------------------------- ### Custom Cache Implementation Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example implementation of a custom cache by inheriting from ICache, using Redis. ```csharp public class RedisCache : ICache { private readonly IConnectionMultiplexer _redis; public bool IsTracked(Guid id) { /* ... */ } public void Set(Guid id, AggregateRoot aggregate) { /* ... */ } public AggregateRoot Get(Guid id) { /* ... */ } public void Remove(Guid id) { /* ... */ } public void RegisterEvictionCallback(Action action) { /* ... */ } } ``` -------------------------------- ### MemoryCache Usage Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Usage example for MemoryCache. ```csharp services.AddSingleton(); ``` -------------------------------- ### RouteRegistrar Usage Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Usage example for RouteRegistrar. ```csharp var registrar = new RouteRegistrar(serviceProvider); // Register all handlers from assembly registrar.Register(typeof(ProductCommandHandler).Assembly); // Register specific types registrar.Register(typeof(ProductCommandHandler), typeof(ProductEventHandler)); ``` -------------------------------- ### Example Aggregate Test Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Demonstrates testing an aggregate in isolation by verifying state changes and event raising. ```csharp [Fact] public void ChangingPrice_RaisesProductPriceChangedEvent() { // Arrange var product = new Product(Guid.NewGuid(), "Test", 100m); product.FlushUncommittedChanges(); // Clear creation event // Act product.ChangePrice(150m); // Assert var events = product.FlushUncommittedChanges(); Assert.Single(events); Assert.IsType(events.First()); Assert.Equal(150m, ((ProductPriceChanged)events.First()).NewPrice); } ``` -------------------------------- ### Usage of ICommandSender Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of using ICommandSender in a controller to send a command. ```csharp public class ProductController { private readonly ICommandSender _commandSender; [HttpPost] public async Task Create(CreateProduct command, CancellationToken token) { await _commandSender.Send(command, token); return Ok(); } } ``` -------------------------------- ### Usage of IEventPublisher Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of using IEventPublisher in an InMemoryEventStore to publish events after saving. ```csharp public class InMemoryEventStore : IEventStore { private readonly IEventPublisher _publisher; public async Task Save(IEnumerable events, CancellationToken cancellationToken) { foreach (var @event in events) { // Save to storage _storage.Add(@event); // Publish after save await _publisher.Publish(@event, cancellationToken); } } } ``` -------------------------------- ### ApplyChange Method Usage Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of using the ApplyChange method to record a new event in an aggregate. ```csharp public class Product : AggregateRoot { public void ChangePrice(decimal newPrice) { // Validate business rules if (newPrice < 0) throw new ArgumentException("Price cannot be negative"); // Apply and record event ApplyChange(new ProductPriceChanged(Id, newPrice)); } } ``` -------------------------------- ### Testing Event Handlers Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of a unit test for an event handler, verifying that an event is correctly processed and added to a read model. ```csharp [Fact] public async Task ProductCreated_AddsToReadModel() { // Arrange var mockDatabase = new Mock(); var handler = new ProductListView(mockDatabase.Object); var @event = new ProductCreated { Id = Guid.NewGuid(), Name = "Test", Price = 100m, Version = 1, TimeStamp = DateTimeOffset.UtcNow }; // Act await handler.Handle(@event, CancellationToken.None); // Assert mockDatabase.Verify(db => db.Insert( It.Is(p => p.Name == "Test" && p.Price == 100m), It.IsAny()), Times.Once); } ``` -------------------------------- ### Usage in Controllers/Application Layer Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of using cqrslite command sender and query processor in an ASP.NET Core controller. ```csharp [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ICommandSender _commandSender; private readonly IQueryProcessor _queryProcessor; public ProductsController(ICommandSender commandSender, IQueryProcessor queryProcessor) { _commandSender = commandSender; _queryProcessor = queryProcessor; } [HttpPost] public async Task Create([FromBody] CreateProduct command, CancellationToken token) { await _commandSender.Send(command, token); return CreatedAtAction(nameof(Get), new { id = command.Id }, null); } [HttpGet("{id}")] public async Task> Get(Guid id) { var result = await _queryProcessor.Query(new GetProduct { Id = id }); return Ok(result); } } ``` -------------------------------- ### Implementing Caching Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Code examples for adding a caching layer for frequently accessed aggregates using MemoryCache and CacheRepository, including an option to combine caching with snapshots. ```csharp services.AddSingleton(); services.AddScoped(sp => new CacheRepository( new Repository(sp.GetService()), sp.GetService(), sp.GetService())); // Or combine with snapshots: services.AddScoped(sp => new CacheRepository( new SnapshotRepository( sp.GetService(), sp.GetService(), new Repository(sp.GetService()), sp.GetService()), sp.GetService(), sp.GetService())); ``` -------------------------------- ### Manual Handler Registration Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example of manually registering a message handler using IHandlerRegistrar. ```csharp var registrar = serviceProvider.GetService(); registrar.RegisterHandler(async (cmd, token) => { var handler = serviceProvider.GetService(); await handler.Handle(cmd, token); }); ``` -------------------------------- ### Manual Handler Registration Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of manually registering command and event handlers using IHandlerRegistrar instead of automatic registration. ```csharp var registrar = serviceProvider.GetService(); // Register command handler registrar.RegisterHandler(async (message, token) => { var handler = serviceProvider.GetService(); await handler.Handle((CreateProduct)message); }); // Register event handler registrar.RegisterHandler(async (message, token) => { var handler = serviceProvider.GetService(); await handler.Handle((ProductCreated)message, token); }); ``` -------------------------------- ### Aggregate Root Constructor Requirements Source: https://github.com/gautema/cqrslite/blob/master/API_REFERENCE.md Example demonstrating constructor requirements for AggregateRoot subclasses, including one for new aggregates and a private parameterless one for rehydration. ```csharp public class Product : AggregateRoot { // Constructor for new aggregates public Product(Guid id, string name, decimal price) { Id = id; ApplyChange(new ProductCreated(id, name, price)); } // Required for rehydration private Product() { } } ``` -------------------------------- ### Unit Testing Aggregates Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Provides examples of unit tests for aggregate behavior, including creation, price changes, and handling invalid operations. ```csharp public class ProductTests { [Fact] public void CreatingProduct_RaisesProductCreatedEvent() { var id = Guid.NewGuid(); var product = new Product(id, "Test Product", 99.99m); var events = product.FlushUncommittedChanges(); Assert.Single(events); var createdEvent = Assert.IsType(events.First()); Assert.Equal(id, createdEvent.Id); Assert.Equal("Test Product", createdEvent.Name); Assert.Equal(99.99m, createdEvent.Price); } [Fact] public void ChangingPrice_UpdatesPrice() { var product = new Product(Guid.NewGuid(), "Test", 100m); product.FlushUncommittedChanges(); product.ChangePrice(150m); var events = product.FlushUncommittedChanges(); Assert.Single(events); Assert.IsType(events.First()); } [Fact] public void ChangingPriceOfDiscontinuedProduct_ThrowsException() { var product = new Product(Guid.NewGuid(), "Test", 100m); product.Discontinue(); Assert.Throws(() => product.ChangePrice(150m)); } } ``` -------------------------------- ### Custom Event Application Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example of overriding ApplyEvent for custom event routing within an aggregate. ```csharp public class CustomAggregate : AggregateRoot { protected override void ApplyEvent(IEvent @event) { // Custom routing logic switch (@event) { case CustomEvent1 e: ApplyCustomEvent1(e); break; case CustomEvent2 e: ApplyCustomEvent2(e); break; default: base.ApplyEvent(@event); // Fall back to convention-based routing break; } } } ``` -------------------------------- ### Command Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Defines a command to create a product. ```csharp public class CreateProduct : ICommand { public Guid Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } ``` -------------------------------- ### Handling Business Rule Violations Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Example of handling business rule violations (InvalidOperationException) and validation errors (ArgumentException). ```csharp public async Task UpdatePrice(UpdateProductPrice command) { try { await _commandSender.Send(command); return Ok(); } catch (InvalidOperationException ex) { // Business rule violation from aggregate return BadRequest(new { message = ex.Message }); } catch (ArgumentException ex) { // Validation error return BadRequest(new { message = ex.Message }); } } ``` -------------------------------- ### Aggregate Design - Meaningful Events Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Example illustrating good vs. bad event design for clarity in aggregate design. ```csharp // Good: Business intent is clear public class OrderPlaced : IEvent { /* ... */ } public class OrderShipped : IEvent { /* ... */ } // Bad: Generic state change public class OrderStateChanged : IEvent { public string NewState { get; set; } } ``` -------------------------------- ### Command Handler Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Implements command handlers for creating and updating products. ```csharp public class ProductCommandHandlers : ICommandHandler, ICommandHandler { private readonly ISession _session; public ProductCommandHandlers(ISession session) { _session = session; } public async Task Handle(CreateProduct message) { var product = new Product(message.Id, message.Name, message.Price); await _session.Add(product); await _session.Commit(); } public async Task Handle(UpdateProductPrice message) { var product = await _session.Get(message.Id, message.ExpectedVersion); product.ChangePrice(message.NewPrice); await _session.Commit(); } } ``` -------------------------------- ### Minimal Event Store Implementation Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md A basic implementation of the `IEventStore` interface using SQL. ```csharp public class SqlEventStore : IEventStore { private readonly IEventPublisher _publisher; private readonly IDbConnection _connection; public SqlEventStore(IEventPublisher publisher, IDbConnection connection) { _publisher = publisher; _connection = connection; } public async Task Save(IEnumerable events, CancellationToken cancellationToken = default) { foreach (var @event in events) { // Serialize and save event await _connection.ExecuteAsync( "INSERT INTO Events (AggregateId, Version, Type, Data, Timestamp) VALUES (@Id, @Version, @Type, @Data, @TimeStamp)", new { @event.Id, @event.Version, Type = @event.GetType().AssemblyQualifiedName, Data = JsonSerializer.Serialize(@event), @event.TimeStamp }); // Publish event after saving (important for consistency) await _publisher.Publish(@event, cancellationToken); } } public async Task> Get(Guid aggregateId, int fromVersion, CancellationToken cancellationToken = default) { var eventData = await _connection.QueryAsync( "SELECT * FROM Events WHERE AggregateId = @AggregateId AND Version > @FromVersion ORDER BY Version", new { AggregateId = aggregateId, FromVersion = fromVersion }); return eventData.Select(e => (IEvent)JsonSerializer.Deserialize(e.Data, Type.GetType(e.Type))); } } ``` -------------------------------- ### Implementing Snapshots Source: https://github.com/gautema/cqrslite/blob/master/DEVELOPER.md Demonstrates how to implement snapshots for aggregates to improve performance, including defining a snapshot class, inheriting from SnapshotAggregateRoot, implementing ISnapshotStore, and configuring with SnapshotRepository. ```csharp // 1. Define snapshot class public class ProductSnapshot : Snapshot { public string Name { get; set; } public decimal Price { get; set; } public bool Discontinued { get; set; } } // 2. Change aggregate to inherit from SnapshotAggregateRoot public class Product : SnapshotAggregateRoot { private string _name; private decimal _price; private bool _discontinued; protected override ProductSnapshot CreateSnapshot() { return new ProductSnapshot { Id = Id, Version = Version, Name = _name, Price = _price, Discontinued = _discontinued }; } protected override void RestoreFromSnapshot(ProductSnapshot snapshot) { _name = snapshot.Name; _price = snapshot.Price; _discontinued = snapshot.Discontinued; } } // 3. Implement ISnapshotStore public class SqlSnapshotStore : ISnapshotStore { public async Task Get(Guid id, CancellationToken cancellationToken = default) { // Retrieve and deserialize snapshot from database } public async Task Save(Snapshot snapshot, CancellationToken cancellationToken = default) { // Serialize and save snapshot to database } } // 4. Configure with SnapshotRepository services.AddScoped(); services.AddScoped(); // Snapshots every 100 events services.AddScoped(sp => new SnapshotRepository( sp.GetService(), sp.GetService(), new Repository(sp.GetService()), sp.GetService())); ``` -------------------------------- ### Basic Aggregate Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Implements a basic aggregate root for a product, handling creation and price changes. ```csharp public class Product : AggregateRoot { private string _name; private decimal _price; private bool _discontinued; // Constructor for new aggregates public Product(Guid id, string name, decimal price) { Id = id; ApplyChange(new ProductCreated(id, name, price)); } // Required parameterless constructor for rehydration private Product() { } // Business logic method public void ChangePrice(decimal newPrice) { if (_discontinued) throw new InvalidOperationException("Cannot change price of discontinued product"); if (newPrice < 0) throw new ArgumentException("Price cannot be negative"); ApplyChange(new ProductPriceChanged(Id, newPrice)); } // Convention-based event application private void Apply(ProductCreated e) { _name = e.Name; _price = e.Price; _discontinued = false; } private void Apply(ProductPriceChanged e) { _price = e.NewPrice; } private void Apply(ProductDiscontinued e) { _discontinued = true; } } ``` -------------------------------- ### Direct Repository Usage Source: https://github.com/gautema/cqrslite/blob/master/QUICK_REFERENCE.md Illustrates direct usage of the IRepository for saving and retrieving aggregates. ```csharp public async Task Handle(CreateProduct command) { var product = new Product(command.Id, command.Name, command.Price); await _repository.Save(product); } public async Task Handle(UpdateProduct command) { var product = await _repository.Get(command.Id); product.Update(command.Name, command.Price); await _repository.Save(product, command.ExpectedVersion); } ```