using System.Collections.Immutable; using CsharpEs.Domain; using CsharpEs.Library; namespace CsharpEs.Infrastructure; public abstract record ReadModelError { public sealed record AccountDetailsNotFound : ReadModelError; } public sealed record AccountBalanceReadModel( Func> Project, Func[], ReadModelError>> Query ); [Flags] public enum AccountFlags { None = 0, InOverdraft = 1, } public sealed record AccountDetails(Money Balance, AccountFlags Flags); public static class AccountBalanceReadModelModule { public static Result Create() { // This is our read-model specific "persistent" storage var modelData = ImmutableDictionary.Empty; Result Project(AccountEvent @event) { Logging.Output("rm", "Projecting event", @event); switch (@event) { case AccountEvent.AccountOpened e: modelData = modelData.SetItem( e.AccountId, new AccountDetails(e.OpeningBalance, AccountFlags.None) ); break; case AccountEvent.MoneyWithdrawn e: { if (modelData.TryGetValue(e.AccountId, out var oldDetails)) { var newBalance = new Money(oldDetails.Balance.Amount - e.Amount.Amount); modelData = modelData.SetItem( e.AccountId, oldDetails with { Balance = newBalance, Flags = CalcAccountFlags(newBalance, oldDetails), } ); } else return Result.Fail( new ReadModelError.AccountDetailsNotFound() ); } break; case AccountEvent.MoneyDeposited e: { if (modelData.TryGetValue(e.AccountId, out var oldDetails)) { var newBalance = new Money(oldDetails.Balance.Amount + e.Amount.Amount); modelData = modelData.SetItem( e.AccountId, oldDetails with { Balance = newBalance, Flags = CalcAccountFlags(newBalance, oldDetails), } ); } else return Result.Fail( new ReadModelError.AccountDetailsNotFound() ); } break; } // Read model-side "project" may not normally return any data, but for demo // purposes we return the affected data directly to save us modeling a separate // query side. return Result.Ok(modelData[@event.AccountId]); } Result[], ReadModelError> Query() => Result[], ReadModelError>.Ok(modelData.ToArray()); return Result.Ok( new AccountBalanceReadModel(Project, Query) ); } private static AccountFlags CalcAccountFlags(Money newBalance, AccountDetails oldDetails) { return newBalance.Amount < 0 ? oldDetails.Flags | AccountFlags.InOverdraft : oldDetails.Flags & ~AccountFlags.InOverdraft; } }