diff --git a/EventSourcing.NetCore.sln b/EventSourcing.NetCore.sln index 806af24d2..e03f70888 100644 --- a/EventSourcing.NetCore.sln +++ b/EventSourcing.NetCore.sln @@ -293,8 +293,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "07-BusinessLogic", "Worksho EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "07-BusinessLogic", "Workshops\IntroductionToEventSourcing\Solved\07-BusinessLogic\07-BusinessLogic.csproj", "{2B630D31-F68C-447E-82C4-48E2B17322B5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "09-BusinessLogic.EventStoreDB", "Workshops\IntroductionToEventSourcing\Solved\09-BusinessLogic.EventStoreDB\09-BusinessLogic.EventStoreDB.csproj", "{26E884E8-4E81-4C87-A643-FE7327F218CE}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "10-OptimisticConcurrency.Marten", "Workshops\IntroductionToEventSourcing\10-OptimisticConcurrency.Marten\10-OptimisticConcurrency.Marten.csproj", "{DFC5BD15-8718-469A-B0FE-D21E473F1133}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "10-OptimisticConcurrency.Marten", "Workshops\IntroductionToEventSourcing\Solved\10-OptimisticConcurrency.Marten\10-OptimisticConcurrency.Marten.csproj", "{C33A067F-5335-4011-B955-66901D877B24}" @@ -815,10 +813,6 @@ Global {2B630D31-F68C-447E-82C4-48E2B17322B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B630D31-F68C-447E-82C4-48E2B17322B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B630D31-F68C-447E-82C4-48E2B17322B5}.Release|Any CPU.Build.0 = Release|Any CPU - {26E884E8-4E81-4C87-A643-FE7327F218CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {26E884E8-4E81-4C87-A643-FE7327F218CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {26E884E8-4E81-4C87-A643-FE7327F218CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {26E884E8-4E81-4C87-A643-FE7327F218CE}.Release|Any CPU.Build.0 = Release|Any CPU {DFC5BD15-8718-469A-B0FE-D21E473F1133}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DFC5BD15-8718-469A-B0FE-D21E473F1133}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFC5BD15-8718-469A-B0FE-D21E473F1133}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1164,7 +1158,6 @@ Global {0FE43045-AC67-4DB9-9820-82C9D55EA09A} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB} {2FFCF9EC-9C75-497B-BC94-B0051367780F} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB} {2B630D31-F68C-447E-82C4-48E2B17322B5} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E} - {26E884E8-4E81-4C87-A643-FE7327F218CE} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E} {DFC5BD15-8718-469A-B0FE-D21E473F1133} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB} {C33A067F-5335-4011-B955-66901D877B24} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E} {F21F4280-8B2A-43E1-8044-3E6E2905F60B} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/09-BusinessLogic.EventStoreDB.csproj b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/09-BusinessLogic.EventStoreDB.csproj deleted file mode 100644 index b03d9075a..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/09-BusinessLogic.EventStoreDB.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - net8.0 - IntroductionToEventSourcing.BusinessLogic - - - - - - - - - all - runtime; build; native; contentfiles; analyzers - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/BusinessLogic.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/BusinessLogic.cs deleted file mode 100644 index 7dae9f2e9..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/BusinessLogic.cs +++ /dev/null @@ -1,263 +0,0 @@ -namespace IntroductionToEventSourcing.BusinessLogic.Immutable; -using static ShoppingCartEvent; - -// ENTITY -public record ShoppingCart( - Guid Id, - Guid ClientId, - ShoppingCartStatus Status, - PricedProductItem[] ProductItems, - DateTime? ConfirmedAt = null, - DateTime? CanceledAt = null -) -{ - public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); - - public static ShoppingCart Default() => - new (default, default, default, []); - - public static string StreamName(Guid id) => $"shopping_cart-{id}"; - - public static ShoppingCart Evolve(ShoppingCart shoppingCart, object @event) - { - return @event switch - { - ShoppingCartOpened(var shoppingCartId, var clientId) => - shoppingCart with - { - Id = shoppingCartId, - ClientId = clientId, - Status = ShoppingCartStatus.Pending - }, - ProductItemAddedToShoppingCart(_, var pricedProductItem) => - shoppingCart with - { - ProductItems = shoppingCart.ProductItems - .Concat(new [] { pricedProductItem }) - .GroupBy(pi => pi.ProductId) - .Select(group => group.Count() == 1? - group.First() - : new PricedProductItem( - group.Key, - group.Sum(pi => pi.Quantity), - group.First().UnitPrice - ) - ) - .ToArray() - }, - ProductItemRemovedFromShoppingCart(_, var pricedProductItem) => - shoppingCart with - { - ProductItems = shoppingCart.ProductItems - .Select(pi => pi.ProductId == pricedProductItem.ProductId? - pi with { Quantity = pi.Quantity - pricedProductItem.Quantity } - :pi - ) - .Where(pi => pi.Quantity > 0) - .ToArray() - }, - ShoppingCartConfirmed(_, var confirmedAt) => - shoppingCart with - { - Status = ShoppingCartStatus.Confirmed, - ConfirmedAt = confirmedAt - }, - ShoppingCartCanceled(_, var canceledAt) => - shoppingCart with - { - Status = ShoppingCartStatus.Canceled, - CanceledAt = canceledAt - }, - _ => shoppingCart - }; - } - - public bool HasEnough(PricedProductItem productItem) - { - var (productId, quantity, _) = productItem; - var currentQuantity = ProductItems.Where(pi => pi.ProductId == productId) - .Select(pi => pi.Quantity) - .FirstOrDefault(); - - return currentQuantity >= quantity; - } -} - -public enum ShoppingCartStatus -{ - Pending = 1, - Confirmed = 2, - Canceled = 4, - - Closed = Confirmed | Canceled -} - -public record OpenShoppingCart( - Guid ShoppingCartId, - Guid ClientId -) -{ - public static OpenShoppingCart From(Guid? cartId, Guid? clientId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (clientId == null || clientId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(clientId)); - - return new OpenShoppingCart(cartId.Value, clientId.Value); - } - - public static ShoppingCartOpened Handle(OpenShoppingCart command) - { - var (shoppingCartId, clientId) = command; - - return new ShoppingCartOpened( - shoppingCartId, - clientId - ); - } -} - -public record AddProductItemToShoppingCart( - Guid ShoppingCartId, - ProductItem ProductItem -) -{ - public static AddProductItemToShoppingCart From(Guid? cartId, ProductItem? productItem) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (productItem == null) - throw new ArgumentOutOfRangeException(nameof(productItem)); - - return new AddProductItemToShoppingCart(cartId.Value, productItem); - } - - public static ProductItemAddedToShoppingCart Handle( - IProductPriceCalculator productPriceCalculator, - AddProductItemToShoppingCart command, - ShoppingCart shoppingCart - ) - { - var (cartId, productItem) = command; - - if (shoppingCart.IsClosed) - throw new InvalidOperationException( - $"Adding product item for cart in '{shoppingCart.Status}' status is not allowed."); - - var pricedProductItem = productPriceCalculator.Calculate(productItem); - - return new ProductItemAddedToShoppingCart( - cartId, - pricedProductItem - ); - } -} - -public record RemoveProductItemFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -) -{ - public static RemoveProductItemFromShoppingCart From(Guid? cartId, PricedProductItem? productItem) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (productItem == null) - throw new ArgumentOutOfRangeException(nameof(productItem)); - - return new RemoveProductItemFromShoppingCart(cartId.Value, productItem); - } - - public static ProductItemRemovedFromShoppingCart Handle( - RemoveProductItemFromShoppingCart command, - ShoppingCart shoppingCart - ) - { - var (cartId, productItem) = command; - - if (shoppingCart.IsClosed) - throw new InvalidOperationException( - $"Adding product item for cart in '{shoppingCart.Status}' status is not allowed."); - - if (!shoppingCart.HasEnough(productItem)) - throw new InvalidOperationException("Not enough product items to remove"); - - return new ProductItemRemovedFromShoppingCart( - cartId, - productItem - ); - } -} - -public record ConfirmShoppingCart( - Guid ShoppingCartId -) -{ - public static ConfirmShoppingCart From(Guid? cartId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - - return new ConfirmShoppingCart(cartId.Value); - } - - public static ShoppingCartConfirmed Handle(ConfirmShoppingCart command, ShoppingCart shoppingCart) - { - if (shoppingCart.IsClosed) - throw new InvalidOperationException($"Confirming cart in '{shoppingCart.Status}' status is not allowed."); - - return new ShoppingCartConfirmed( - shoppingCart.Id, - DateTime.UtcNow - ); - } -} - -public record CancelShoppingCart( - Guid ShoppingCartId -) -{ - public static CancelShoppingCart From(Guid? cartId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - - return new CancelShoppingCart(cartId.Value); - } - - public static ShoppingCartCanceled Handle(CancelShoppingCart command, ShoppingCart shoppingCart) - { - if (shoppingCart.IsClosed) - throw new InvalidOperationException($"Canceling cart in '{shoppingCart.Status}' status is not allowed."); - - return new ShoppingCartCanceled( - shoppingCart.Id, - DateTime.UtcNow - ); - } -} - -public interface IProductPriceCalculator -{ - PricedProductItem Calculate(ProductItem productItems); -} - -public class FakeProductPriceCalculator: IProductPriceCalculator -{ - private readonly int value; - - private FakeProductPriceCalculator(int value) - { - this.value = value; - } - - public static FakeProductPriceCalculator Returning(int value) => new(value); - - public PricedProductItem Calculate(ProductItem productItem) - { - var (productId, quantity) = productItem; - - return new PricedProductItem(productId, quantity, value); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/BusinessLogicTests.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/BusinessLogicTests.cs deleted file mode 100644 index 5b5b08e52..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/BusinessLogicTests.cs +++ /dev/null @@ -1,152 +0,0 @@ -using FluentAssertions; -using IntroductionToEventSourcing.BusinessLogic.Tools; -using Xunit; - -namespace IntroductionToEventSourcing.BusinessLogic.Immutable; - -// EVENTS -public abstract record ShoppingCartEvent -{ - public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId - ): ShoppingCartEvent; - - public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem - ): ShoppingCartEvent; - - public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem - ): ShoppingCartEvent; - - public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt - ): ShoppingCartEvent; - - public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt - ): ShoppingCartEvent; - - // This won't allow external inheritance - private ShoppingCartEvent(){} -} - -// VALUE OBJECTS - -public record ProductItem( - Guid ProductId, - int Quantity -); - -public record PricedProductItem( - Guid ProductId, - int Quantity, - decimal UnitPrice -); - -// Business logic - -public class BusinessLogicTests: EventStoreDBTest -{ - [Fact] - public async Task GettingState_ForSequenceOfEvents_ShouldSucceed() - { - var shoppingCartId = Guid.NewGuid(); - var clientId = Guid.NewGuid(); - var shoesId = Guid.NewGuid(); - var tShirtId = Guid.NewGuid(); - var twoPairsOfShoes = new ProductItem(shoesId, 2); - var pairOfShoes = new ProductItem(shoesId, 1); - var tShirt = new ProductItem(tShirtId, 1); - - var shoesPrice = 100; - var tShirtPrice = 50; - - var pricedPairOfShoes = new PricedProductItem(shoesId, 1, shoesPrice); - var pricedTShirt = new PricedProductItem(tShirtId, 1, tShirtPrice); - - await EventStore.Add( - command => ShoppingCart.StreamName(command.ShoppingCartId), - OpenShoppingCart.Handle, - OpenShoppingCart.From(shoppingCartId, clientId), - CancellationToken.None - ); - - // Add two pairs of shoes - await EventStore.GetAndUpdate( - ShoppingCart.Evolve, - ShoppingCart.Default(), - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - AddProductItemToShoppingCart.Handle(FakeProductPriceCalculator.Returning(shoesPrice), command, shoppingCart), - AddProductItemToShoppingCart.From(shoppingCartId, twoPairsOfShoes), - CancellationToken.None - ); - - // Add T-Shirt - await EventStore.GetAndUpdate( - ShoppingCart.Evolve, - ShoppingCart.Default(), - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - AddProductItemToShoppingCart.Handle(FakeProductPriceCalculator.Returning(tShirtPrice), command, shoppingCart), - AddProductItemToShoppingCart.From(shoppingCartId, tShirt), - CancellationToken.None - ); - - // Remove pair of shoes - await EventStore.GetAndUpdate( - ShoppingCart.Evolve, - ShoppingCart.Default(), - command => ShoppingCart.StreamName(command.ShoppingCartId), - RemoveProductItemFromShoppingCart.Handle, - RemoveProductItemFromShoppingCart.From(shoppingCartId, pricedPairOfShoes), - CancellationToken.None - ); - - // Confirm - await EventStore.GetAndUpdate( - ShoppingCart.Evolve, - ShoppingCart.Default(), - command => ShoppingCart.StreamName(command.ShoppingCartId), - ConfirmShoppingCart.Handle, - ConfirmShoppingCart.From(shoppingCartId), - CancellationToken.None - ); - - // Cancel - var exception = await Record.ExceptionAsync(async () => - { - await EventStore.GetAndUpdate( - ShoppingCart.Evolve, - ShoppingCart.Default(), - command => ShoppingCart.StreamName(command.ShoppingCartId), - CancelShoppingCart.Handle, - CancelShoppingCart.From(shoppingCartId), - CancellationToken.None - ); - } - ); - exception.Should().BeOfType(); - - var shoppingCart = await EventStore.Get( - ShoppingCart.Evolve, - ShoppingCart.Default(), - ShoppingCart.StreamName(shoppingCartId), - CancellationToken.None - ); - - shoppingCart.Id.Should().Be(shoppingCartId); - shoppingCart.ClientId.Should().Be(clientId); - shoppingCart.ProductItems.Should().HaveCount(2); - shoppingCart.Status.Should().Be(ShoppingCartStatus.Confirmed); - - shoppingCart.ProductItems[0].Should().Be(pricedPairOfShoes); - shoppingCart.ProductItems[1].Should().Be(pricedTShirt); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/EventStoreClientExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/EventStoreClientExtensions.cs deleted file mode 100644 index 751cb0a35..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Immutable/EventStoreClientExtensions.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Text.Json; -using EventStore.Client; - -namespace IntroductionToEventSourcing.BusinessLogic.Immutable; - -public static class EventStoreClientExtensions -{ - public static async Task Get( - this EventStoreClient eventStore, - Func when, - TEntity empty, - string streamName, - CancellationToken cancellationToken = default - ) - { - var result = eventStore.ReadStreamAsync( - Direction.Forwards, - streamName, - StreamPosition.Start, - cancellationToken: cancellationToken - ); - - if (await result.ReadState == ReadState.StreamNotFound) - throw new InvalidOperationException("Shopping Cart was not found!"); - - return await result - .Select(@event => - JsonSerializer.Deserialize( - @event.Event.Data.Span, - Type.GetType(@event.Event.EventType)! - )! - ) - .AggregateAsync( - empty, - when, - cancellationToken - ); - } - - public static Task Add( - this EventStoreClient eventStore, - Func getStreamName, - Func action, - TCommand command, - CancellationToken cancellationToken = default - ) - { - var @event = action(command); - - return eventStore.AppendToStreamAsync( - getStreamName(command), - StreamState.Any, - new[] - { - new EventData( - Uuid.NewUuid(), - @event.GetType().FullName!, - JsonSerializer.SerializeToUtf8Bytes(@event) - ) - }, - cancellationToken: cancellationToken - ); - } - - public static async Task GetAndUpdate( - this EventStoreClient eventStore, - Func when, - TEntity empty, - Func getStreamName, - Func action, - TCommand command, - CancellationToken cancellationToken = default - ) - { - var streamName = getStreamName(command); - var current = await eventStore.Get(when, empty, streamName, cancellationToken); - - var @event = action(command, current); - - await eventStore.AppendToStreamAsync( - getStreamName(command), - StreamState.Any, - new[] - { - new EventData( - Uuid.NewUuid(), - @event.GetType().FullName!, - JsonSerializer.SerializeToUtf8Bytes(@event) - ) - }, - cancellationToken: cancellationToken - ); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/BusinessLogic.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/BusinessLogic.cs deleted file mode 100644 index 38816433c..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/BusinessLogic.cs +++ /dev/null @@ -1,294 +0,0 @@ -namespace IntroductionToEventSourcing.BusinessLogic.Mixed; -using static ShoppingCartEvent; - -public interface IAggregate -{ - public Guid Id { get; } - - public void Evolve(object @event) { } -} - - -// COMMANDS -public record OpenShoppingCart( - Guid ShoppingCartId, - Guid ClientId -) -{ - public static OpenShoppingCart From(Guid? cartId, Guid? clientId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (clientId == null || clientId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(clientId)); - - return new OpenShoppingCart(cartId.Value, clientId.Value); - } -} - -public record AddProductItemToShoppingCart( - Guid ShoppingCartId, - ProductItem ProductItem -) -{ - public static AddProductItemToShoppingCart From(Guid? cartId, ProductItem? productItem) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (productItem == null) - throw new ArgumentOutOfRangeException(nameof(productItem)); - - return new AddProductItemToShoppingCart(cartId.Value, productItem); - } -} - -public record RemoveProductItemFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -) -{ - public static RemoveProductItemFromShoppingCart From(Guid? cartId, PricedProductItem? productItem) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (productItem == null) - throw new ArgumentOutOfRangeException(nameof(productItem)); - - return new RemoveProductItemFromShoppingCart(cartId.Value, productItem); - } -} - -public record ConfirmShoppingCart( - Guid ShoppingCartId -) -{ - public static ConfirmShoppingCart From(Guid? cartId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - - return new ConfirmShoppingCart(cartId.Value); - } -} - -public record CancelShoppingCart( - Guid ShoppingCartId -) -{ - public static CancelShoppingCart From(Guid? cartId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - - return new CancelShoppingCart(cartId.Value); - } -} - -public class ShoppingCart: IAggregate -{ - public Guid Id { get; private set; } - public Guid ClientId { get; private set; } - public ShoppingCartStatus Status { get; private set; } - public IList ProductItems { get; } = new List(); - public DateTime? ConfirmedAt { get; private set; } - public DateTime? CanceledAt { get; private set; } - - public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); - - public static string StreamName(Guid id) => $"shopping_cart-{id}"; - - public void Evolve(object @event) - { - switch (@event) - { - case ShoppingCartOpened opened: - Apply(opened); - break; - case ProductItemAddedToShoppingCart productItemAdded: - Apply(productItemAdded); - break; - case ProductItemRemovedFromShoppingCart productItemRemoved: - Apply(productItemRemoved); - break; - case ShoppingCartConfirmed confirmed: - Apply(confirmed); - break; - case ShoppingCartCanceled canceled: - Apply(canceled); - break; - } - } - - public static (ShoppingCartOpened Event, ShoppingCart Aggregate) Open( - Guid cartId, - Guid clientId) - { - var @event = new ShoppingCartOpened( - cartId, - clientId - ); - - - - return (@event, new ShoppingCart(@event)); - } - - private ShoppingCart(ShoppingCartOpened @event) => - Apply(@event); - - //just for default creation of empty object - private ShoppingCart() { } - - private void Apply(ShoppingCartOpened opened) - { - Id = opened.ShoppingCartId; - ClientId = opened.ClientId; - Status = ShoppingCartStatus.Pending; - } - - public ProductItemAddedToShoppingCart AddProduct( - IProductPriceCalculator productPriceCalculator, - ProductItem productItem) - { - if (IsClosed) - throw new InvalidOperationException( - $"Adding product item for cart in '{Status}' status is not allowed."); - - var pricedProductItem = productPriceCalculator.Calculate(productItem); - - var @event = new ProductItemAddedToShoppingCart(Id, pricedProductItem); - - Apply(@event); - - return @event; - } - - private void Apply(ProductItemAddedToShoppingCart productItemAdded) - { - var (_, pricedProductItem) = productItemAdded; - var productId = pricedProductItem.ProductId; - var quantityToAdd = pricedProductItem.Quantity; - - var current = ProductItems.SingleOrDefault( - pi => pi.ProductId == productId - ); - - if (current == null) - ProductItems.Add(pricedProductItem); - else - ProductItems[ProductItems.IndexOf(current)] = - new PricedProductItem(current.ProductId, current.Quantity + quantityToAdd, current.UnitPrice); - } - - public ProductItemRemovedFromShoppingCart RemoveProduct(PricedProductItem productItemToBeRemoved) - { - if (IsClosed) - throw new InvalidOperationException( - $"Removing product item for cart in '{Status}' status is not allowed."); - - if (!HasEnough(productItemToBeRemoved)) - throw new InvalidOperationException("Not enough product items to remove"); - - var @event = new ProductItemRemovedFromShoppingCart(Id, productItemToBeRemoved); - - Apply(@event); - - return @event; - } - - private bool HasEnough(PricedProductItem productItem) - { - var currentQuantity = ProductItems.Where(pi => pi.ProductId == productItem.ProductId) - .Select(pi => pi.Quantity) - .FirstOrDefault(); - - return currentQuantity >= productItem.Quantity; - } - - private void Apply(ProductItemRemovedFromShoppingCart productItemRemoved) - { - var (_, pricedProductItem) = productItemRemoved; - var productId = pricedProductItem.ProductId; - var quantityToRemove = pricedProductItem.Quantity; - - var current = ProductItems.Single( - pi => pi.ProductId == productId - ); - - if (current.Quantity == quantityToRemove) - ProductItems.Remove(current); - else - ProductItems[ProductItems.IndexOf(current)] = - new PricedProductItem(current.ProductId, current.Quantity - quantityToRemove, current.UnitPrice); - } - - public ShoppingCartConfirmed Confirm() - { - if (IsClosed) - throw new InvalidOperationException( - $"Confirming cart in '{Status}' status is not allowed."); - - var @event = new ShoppingCartConfirmed(Id, DateTime.UtcNow); - - Apply(@event); - - return @event; - } - - private void Apply(ShoppingCartConfirmed confirmed) - { - Status = ShoppingCartStatus.Confirmed; - ConfirmedAt = confirmed.ConfirmedAt; - } - - public ShoppingCartCanceled Cancel() - { - if (IsClosed) - throw new InvalidOperationException( - $"Canceling cart in '{Status}' status is not allowed."); - - var @event = new ShoppingCartCanceled(Id, DateTime.UtcNow); - - Apply(@event); - - return @event; - } - - private void Apply(ShoppingCartCanceled canceled) - { - Status = ShoppingCartStatus.Canceled; - CanceledAt = canceled.CanceledAt; - } -} - -public enum ShoppingCartStatus -{ - Pending = 1, - Confirmed = 2, - Canceled = 4, - - Closed = Confirmed | Canceled -} - -public interface IProductPriceCalculator -{ - PricedProductItem Calculate(ProductItem productItems); -} - -public class FakeProductPriceCalculator: IProductPriceCalculator -{ - private readonly int value; - - private FakeProductPriceCalculator(int value) - { - this.value = value; - } - - public static FakeProductPriceCalculator Returning(int value) => new(value); - - public PricedProductItem Calculate(ProductItem productItem) - { - var (productId, quantity) = productItem; - return new PricedProductItem(productId, quantity, value); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/BusinessLogicTests.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/BusinessLogicTests.cs deleted file mode 100644 index 7c6be31a6..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/BusinessLogicTests.cs +++ /dev/null @@ -1,160 +0,0 @@ -using FluentAssertions; -using IntroductionToEventSourcing.BusinessLogic.Tools; -using Xunit; - -namespace IntroductionToEventSourcing.BusinessLogic.Mixed; - -// EVENTS -public abstract record ShoppingCartEvent -{ - public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId - ): ShoppingCartEvent; - - public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem - ): ShoppingCartEvent; - - public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem - ): ShoppingCartEvent; - - public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt - ): ShoppingCartEvent; - - public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt - ): ShoppingCartEvent; - - // This won't allow external inheritance - private ShoppingCartEvent(){} -} - -// VALUE OBJECTS -public record ProductItem( - Guid ProductId, - int Quantity -); - -public record PricedProductItem( - Guid ProductId, - int Quantity, - decimal UnitPrice -); - -public static class ShoppingCartExtensions -{ - public static ShoppingCart GetShoppingCart(this IEnumerable events) - { - var shoppingCart = (ShoppingCart)Activator.CreateInstance(typeof(ShoppingCart), true)!; - - foreach (var @event in events) - { - shoppingCart.Evolve(@event); - } - - return shoppingCart; - } -} - -public class BusinessLogicTests: EventStoreDBTest -{ - [Fact] - public async Task GettingState_ForSequenceOfEvents_ShouldSucceed() - { - var shoppingCartId = Guid.NewGuid(); - var clientId = Guid.NewGuid(); - var shoesId = Guid.NewGuid(); - var tShirtId = Guid.NewGuid(); - - var twoPairsOfShoes = new ProductItem(shoesId, 2); - var pairOfShoes = new ProductItem(shoesId, 1); - var tShirt = new ProductItem(tShirtId, 1); - - var shoesPrice = 100; - var tShirtPrice = 50; - - var pricedPairOfShoes = new PricedProductItem(shoesId, 1, shoesPrice); - var pricedTShirt = new PricedProductItem(tShirtId, 1, tShirtPrice); - - // Open - await EventStore.Add( - command => ShoppingCart.StreamName(command.ShoppingCartId), - command => - ShoppingCart.Open(command.ShoppingCartId, command.ClientId).Event, - OpenShoppingCart.From(shoppingCartId, clientId), - CancellationToken.None - ); - - // Add two pairs of shoes - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - shoppingCart.AddProduct(FakeProductPriceCalculator.Returning(shoesPrice), command.ProductItem), - AddProductItemToShoppingCart.From(shoppingCartId, twoPairsOfShoes), - CancellationToken.None - ); - - // Add T-Shirt - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - shoppingCart.AddProduct(FakeProductPriceCalculator.Returning(tShirtPrice), command.ProductItem), - AddProductItemToShoppingCart.From(shoppingCartId, tShirt), - CancellationToken.None - ); - - // Remove pair of shoes - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - shoppingCart.RemoveProduct(command.ProductItem), - RemoveProductItemFromShoppingCart.From(shoppingCartId, pricedPairOfShoes), - CancellationToken.None - ); - - // Confirm - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (_, shoppingCart) => - shoppingCart.Confirm(), - ConfirmShoppingCart.From(shoppingCartId), - CancellationToken.None - ); - - // Cancel - var exception = await Record.ExceptionAsync(async () => - { - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (_, shoppingCart) => - shoppingCart.Cancel(), - CancelShoppingCart.From(shoppingCartId), - CancellationToken.None - ); - } - ); - exception.Should().BeOfType(); - - var shoppingCart = await EventStore.Get(ShoppingCart.StreamName(shoppingCartId), CancellationToken.None); - - shoppingCart.Id.Should().Be(shoppingCartId); - shoppingCart.ClientId.Should().Be(clientId); - shoppingCart.ProductItems.Should().HaveCount(2); - shoppingCart.Status.Should().Be(ShoppingCartStatus.Confirmed); - - shoppingCart.ProductItems[0].ProductId.Should().Be(shoesId); - shoppingCart.ProductItems[0].Quantity.Should().Be(pricedPairOfShoes.Quantity); - shoppingCart.ProductItems[0].UnitPrice.Should().Be(pricedPairOfShoes.UnitPrice); - - shoppingCart.ProductItems[1].ProductId.Should().Be(tShirtId); - shoppingCart.ProductItems[1].Quantity.Should().Be(pricedTShirt.Quantity); - shoppingCart.ProductItems[1].UnitPrice.Should().Be(pricedTShirt.UnitPrice); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/EventStoreClientExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/EventStoreClientExtensions.cs deleted file mode 100644 index 8c35c833c..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mixed/EventStoreClientExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Text.Json; -using EventStore.Client; - -namespace IntroductionToEventSourcing.BusinessLogic.Mixed; - -public static class EventStoreClientExtensions -{ - public static async Task Get( - this EventStoreClient eventStore, - string streamName, - CancellationToken cancellationToken = default - ) where TAggregate : IAggregate - { - var result = eventStore.ReadStreamAsync( - Direction.Forwards, - streamName, - StreamPosition.Start, - cancellationToken: cancellationToken - ); - - if (await result.ReadState == ReadState.StreamNotFound) - throw new InvalidOperationException("Shopping Cart was not found!"); - - var empty = (TAggregate)Activator.CreateInstance(typeof(TAggregate), true)!; - - return await result - .Select(@event => - JsonSerializer.Deserialize( - @event.Event.Data.Span, - Type.GetType(@event.Event.EventType)! - )! - ) - .AggregateAsync( - empty, - ((aggregate, @event) => - { - aggregate.Evolve(@event!); - return aggregate; - }), - cancellationToken - ); - } - - public static Task Add( - this EventStoreClient eventStore, - Func getStreamName, - Func action, - TCommand command, - CancellationToken cancellationToken = default - ) where TAggregate : IAggregate - { - var @event = action(command); - - return eventStore.AppendToStreamAsync( - getStreamName(command), - StreamState.Any, - new[] - { - new EventData( - Uuid.NewUuid(), - @event.GetType().FullName!, - JsonSerializer.SerializeToUtf8Bytes(@event) - ) - }, - cancellationToken: cancellationToken - ); - } - - public static async Task GetAndUpdate( - this EventStoreClient eventStore, - Func getStreamName, - Func action, - TCommand command, - CancellationToken cancellationToken = default - ) where TAggregate : IAggregate - { - var streamName = getStreamName(command); - var current = await eventStore.Get(streamName, cancellationToken); - - var @event = action(command, current); - - await eventStore.AppendToStreamAsync( - getStreamName(command), - StreamState.Any, - new[] - { - new EventData( - Uuid.NewUuid(), - @event.GetType().FullName!, - JsonSerializer.SerializeToUtf8Bytes(@event) - ) - }, - cancellationToken: cancellationToken - ); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/BusinessLogic.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/BusinessLogic.cs deleted file mode 100644 index 6cd301cac..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/BusinessLogic.cs +++ /dev/null @@ -1,309 +0,0 @@ -namespace IntroductionToEventSourcing.BusinessLogic.Mutable; -using static ShoppingCartEvent; - -public abstract class Aggregate -{ - public Guid Id { get; protected set; } = default!; - - private readonly Queue uncommittedEvents = new(); - - public virtual void Evolve(object @event) { } - - public object[] DequeueUncommittedEvents() - { - var dequeuedEvents = uncommittedEvents.ToArray(); - - uncommittedEvents.Clear(); - - return dequeuedEvents; - } - - protected void Enqueue(object @event) - { - uncommittedEvents.Enqueue(@event); - } -} - -// COMMANDS -public record OpenShoppingCart( - Guid ShoppingCartId, - Guid ClientId -) -{ - public static OpenShoppingCart From(Guid? cartId, Guid? clientId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (clientId == null || clientId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(clientId)); - - return new OpenShoppingCart(cartId.Value, clientId.Value); - } -} - -public record AddProductItemToShoppingCart( - Guid ShoppingCartId, - ProductItem ProductItem -) -{ - public static AddProductItemToShoppingCart From(Guid? cartId, ProductItem? productItem) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (productItem == null) - throw new ArgumentOutOfRangeException(nameof(productItem)); - - return new AddProductItemToShoppingCart(cartId.Value, productItem); - } -} - -public record RemoveProductItemFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -) -{ - public static RemoveProductItemFromShoppingCart From(Guid? cartId, PricedProductItem? productItem) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - if (productItem == null) - throw new ArgumentOutOfRangeException(nameof(productItem)); - - return new RemoveProductItemFromShoppingCart(cartId.Value, productItem); - } -} - -public record ConfirmShoppingCart( - Guid ShoppingCartId -) -{ - public static ConfirmShoppingCart From(Guid? cartId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - - return new ConfirmShoppingCart(cartId.Value); - } -} - -public record CancelShoppingCart( - Guid ShoppingCartId -) -{ - public static CancelShoppingCart From(Guid? cartId) - { - if (cartId == null || cartId == Guid.Empty) - throw new ArgumentOutOfRangeException(nameof(cartId)); - - return new CancelShoppingCart(cartId.Value); - } -} - -public class ShoppingCart: Aggregate -{ - public Guid ClientId { get; private set; } - public ShoppingCartStatus Status { get; private set; } - public IList ProductItems { get; } = new List(); - public DateTime? ConfirmedAt { get; private set; } - public DateTime? CanceledAt { get; private set; } - - public bool IsClosed => ShoppingCartStatus.Closed.HasFlag(Status); - - public static string StreamName(Guid id) => $"shopping_cart-{id}"; - - public override void Evolve(object @event) - { - switch (@event) - { - case ShoppingCartOpened opened: - Apply(opened); - break; - case ProductItemAddedToShoppingCart productItemAdded: - Apply(productItemAdded); - break; - case ProductItemRemovedFromShoppingCart productItemRemoved: - Apply(productItemRemoved); - break; - case ShoppingCartConfirmed confirmed: - Apply(confirmed); - break; - case ShoppingCartCanceled canceled: - Apply(canceled); - break; - } - } - - public static ShoppingCart Open( - Guid cartId, - Guid clientId) - { - return new ShoppingCart(cartId, clientId); - } - - private ShoppingCart( - Guid id, - Guid clientId) - { - var @event = new ShoppingCartOpened( - id, - clientId - ); - - Enqueue(@event); - Apply(@event); - } - - //just for default creation of empty object - private ShoppingCart() { } - - private void Apply(ShoppingCartOpened opened) - { - Id = opened.ShoppingCartId; - ClientId = opened.ClientId; - Status = ShoppingCartStatus.Pending; - } - - public void AddProduct( - IProductPriceCalculator productPriceCalculator, - ProductItem productItem) - { - if (IsClosed) - throw new InvalidOperationException( - $"Adding product item for cart in '{Status}' status is not allowed."); - - var pricedProductItem = productPriceCalculator.Calculate(productItem); - - var @event = new ProductItemAddedToShoppingCart(Id, pricedProductItem); - - Enqueue(@event); - Apply(@event); - } - - private void Apply(ProductItemAddedToShoppingCart productItemAdded) - { - var (_, pricedProductItem) = productItemAdded; - var productId = pricedProductItem.ProductId; - var quantityToAdd = pricedProductItem.Quantity; - - var current = ProductItems.SingleOrDefault( - pi => pi.ProductId == productId - ); - - if (current == null) - ProductItems.Add(pricedProductItem); - else - current.Quantity += quantityToAdd; - } - - public void RemoveProduct(PricedProductItem productItemToBeRemoved) - { - if (IsClosed) - throw new InvalidOperationException( - $"Removing product item for cart in '{Status}' status is not allowed."); - - if (!HasEnough(productItemToBeRemoved)) - throw new InvalidOperationException("Not enough product items to remove"); - - var @event = new ProductItemRemovedFromShoppingCart(Id, productItemToBeRemoved); - - Enqueue(@event); - Apply(@event); - } - - private bool HasEnough(PricedProductItem productItem) - { - var currentQuantity = ProductItems.Where(pi => pi.ProductId == productItem.ProductId) - .Select(pi => pi.Quantity) - .FirstOrDefault(); - - return currentQuantity >= productItem.Quantity; - } - - private void Apply(ProductItemRemovedFromShoppingCart productItemRemoved) - { - var (_, pricedProductItem) = productItemRemoved; - var productId = pricedProductItem.ProductId; - var quantityToRemove = pricedProductItem.Quantity; - - var current = ProductItems.Single( - pi => pi.ProductId == productId - ); - - if (current.Quantity == quantityToRemove) - ProductItems.Remove(current); - else - current.Quantity -= quantityToRemove; - } - - public void Confirm() - { - if (IsClosed) - throw new InvalidOperationException( - $"Confirming cart in '{Status}' status is not allowed."); - - var @event = new ShoppingCartConfirmed(Id, DateTime.UtcNow); - - Enqueue(@event); - Apply(@event); - } - - private void Apply(ShoppingCartConfirmed confirmed) - { - Status = ShoppingCartStatus.Confirmed; - ConfirmedAt = confirmed.ConfirmedAt; - } - - public void Cancel() - { - if (IsClosed) - throw new InvalidOperationException( - $"Canceling cart in '{Status}' status is not allowed."); - - var @event = new ShoppingCartCanceled(Id, DateTime.UtcNow); - - Enqueue(@event); - Apply(@event); - } - - private void Apply(ShoppingCartCanceled canceled) - { - Status = ShoppingCartStatus.Canceled; - CanceledAt = canceled.CanceledAt; - } -} - -public enum ShoppingCartStatus -{ - Pending = 1, - Confirmed = 2, - Canceled = 4, - - Closed = Confirmed | Canceled -} - -public interface IProductPriceCalculator -{ - PricedProductItem Calculate(ProductItem productItems); -} - -public class FakeProductPriceCalculator: IProductPriceCalculator -{ - private readonly int value; - - private FakeProductPriceCalculator(int value) - { - this.value = value; - } - - public static FakeProductPriceCalculator Returning(int value) => new(value); - - public PricedProductItem Calculate(ProductItem productItem) - { - return new PricedProductItem - { - ProductId = productItem.ProductId, - Quantity = productItem.Quantity, - UnitPrice = value - }; - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/BusinessLogicTests.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/BusinessLogicTests.cs deleted file mode 100644 index 319dd6e81..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/BusinessLogicTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using FluentAssertions; -using IntroductionToEventSourcing.BusinessLogic.Tools; -using Xunit; - -namespace IntroductionToEventSourcing.BusinessLogic.Mutable; - -// EVENTS -public abstract record ShoppingCartEvent -{ - public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId - ): ShoppingCartEvent; - - public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem - ): ShoppingCartEvent; - - public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem - ): ShoppingCartEvent; - - public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt - ): ShoppingCartEvent; - - public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt - ): ShoppingCartEvent; - - // This won't allow external inheritance - private ShoppingCartEvent(){} -} - -// VALUE OBJECTS -public class PricedProductItem -{ - public Guid ProductId { get; set; } - public decimal UnitPrice { get; set; } - public int Quantity { get; set; } - public decimal TotalPrice => Quantity * UnitPrice; -} - -public class ProductItem -{ - public Guid ProductId { get; set; } - public int Quantity { get; set; } -} - -public class BusinessLogicTests: EventStoreDBTest -{ - [Fact] - public async Task GettingState_ForSequenceOfEvents_ShouldSucceed() - { - var shoppingCartId = Guid.NewGuid(); - var clientId = Guid.NewGuid(); - var shoesId = Guid.NewGuid(); - var tShirtId = Guid.NewGuid(); - - var twoPairsOfShoes = new ProductItem { ProductId = shoesId, Quantity = 2 }; - var pairOfShoes = new ProductItem { ProductId = shoesId, Quantity = 1 }; - var tShirt = new ProductItem { ProductId = tShirtId, Quantity = 1 }; - - var shoesPrice = 100; - var tShirtPrice = 50; - - var pricedPairOfShoes = new PricedProductItem { ProductId = shoesId, Quantity = 1, UnitPrice = shoesPrice }; - var pricedTShirt = new PricedProductItem{ ProductId = tShirtId, Quantity = 1, UnitPrice = tShirtPrice }; - - // Open - await EventStore.Add( - command => ShoppingCart.StreamName(command.ShoppingCartId), - command => - ShoppingCart.Open(command.ShoppingCartId, command.ClientId), - OpenShoppingCart.From(shoppingCartId, clientId), - CancellationToken.None - ); - - // Add two pairs of shoes - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - shoppingCart.AddProduct(FakeProductPriceCalculator.Returning(shoesPrice), command.ProductItem), - AddProductItemToShoppingCart.From(shoppingCartId, twoPairsOfShoes), - CancellationToken.None - ); - - // Add T-Shirt - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - shoppingCart.AddProduct(FakeProductPriceCalculator.Returning(tShirtPrice), command.ProductItem), - AddProductItemToShoppingCart.From(shoppingCartId, tShirt), - CancellationToken.None - ); - - // Remove pair of shoes - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (command, shoppingCart) => - shoppingCart.RemoveProduct(command.ProductItem), - RemoveProductItemFromShoppingCart.From(shoppingCartId, pricedPairOfShoes), - CancellationToken.None - ); - - // Confirm - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (_, shoppingCart) => - shoppingCart.Confirm(), - ConfirmShoppingCart.From(shoppingCartId), - CancellationToken.None - ); - - // Cancel - var exception = await Record.ExceptionAsync(async () => - { - await EventStore.GetAndUpdate( - command => ShoppingCart.StreamName(command.ShoppingCartId), - (_, shoppingCart) => - shoppingCart.Cancel(), - CancelShoppingCart.From(shoppingCartId), - CancellationToken.None - ); - } - ); - exception.Should().BeOfType(); - - var shoppingCart = await EventStore.Get(ShoppingCart.StreamName(shoppingCartId), CancellationToken.None); - - shoppingCart.Id.Should().Be(shoppingCartId); - shoppingCart.ClientId.Should().Be(clientId); - shoppingCart.ProductItems.Should().HaveCount(2); - shoppingCart.Status.Should().Be(ShoppingCartStatus.Confirmed); - - shoppingCart.ProductItems[0].ProductId.Should().Be(shoesId); - shoppingCart.ProductItems[0].Quantity.Should().Be(pricedPairOfShoes.Quantity); - shoppingCart.ProductItems[0].UnitPrice.Should().Be(pricedPairOfShoes.UnitPrice); - - shoppingCart.ProductItems[1].ProductId.Should().Be(tShirtId); - shoppingCart.ProductItems[1].Quantity.Should().Be(pricedTShirt.Quantity); - shoppingCart.ProductItems[1].UnitPrice.Should().Be(pricedTShirt.UnitPrice); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/EventStoreClientExtensions.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/EventStoreClientExtensions.cs deleted file mode 100644 index 2eb733dc9..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Mutable/EventStoreClientExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System.Text.Json; -using EventStore.Client; - -namespace IntroductionToEventSourcing.BusinessLogic.Mutable; - -public static class EventStoreClientExtensions -{ - public static async Task Get( - this EventStoreClient eventStore, - string streamName, - CancellationToken cancellationToken = default - ) where TAggregate : Aggregate - { - var result = eventStore.ReadStreamAsync( - Direction.Forwards, - streamName, - StreamPosition.Start, - cancellationToken: cancellationToken - ); - - if (await result.ReadState == ReadState.StreamNotFound) - throw new InvalidOperationException("Shopping Cart was not found!"); - - var empty = (TAggregate)Activator.CreateInstance(typeof(TAggregate), true)!; - - return await result - .Select(@event => - JsonSerializer.Deserialize( - @event.Event.Data.Span, - Type.GetType(@event.Event.EventType)! - )! - ) - .AggregateAsync( - empty, - ((aggregate, @event) => - { - aggregate.Evolve(@event!); - return aggregate; - }), - cancellationToken - ); - } - - public static Task Add( - this EventStoreClient eventStore, - Func getStreamName, - Func action, - TCommand command, - CancellationToken cancellationToken = default - ) where TAggregate : Aggregate - { - var aggregate = action(command); - - return eventStore.AppendToStreamAsync( - getStreamName(command), - StreamState.Any, - aggregate.DequeueUncommittedEvents() - .Select(@event => - new EventData( - Uuid.NewUuid(), - @event.GetType().FullName!, - JsonSerializer.SerializeToUtf8Bytes(@event) - ) - ), - cancellationToken: cancellationToken - ); - } - - public static async Task GetAndUpdate( - this EventStoreClient eventStore, - Func getStreamName, - Action action, - TCommand command, - CancellationToken cancellationToken = default - ) where TAggregate : Aggregate - { - var streamName = getStreamName(command); - var current = await eventStore.Get(streamName, cancellationToken); - - action(command, current); - - await eventStore.AppendToStreamAsync( - getStreamName(command), - StreamState.Any, - current.DequeueUncommittedEvents() - .Select(@event => - new EventData( - Uuid.NewUuid(), - @event.GetType().FullName!, - JsonSerializer.SerializeToUtf8Bytes(@event) - ) - ), - cancellationToken: cancellationToken - ); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/README.md b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/README.md deleted file mode 100644 index c8028a20f..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Exercise 09 - Business Logic with EventStoreDB - -Having the following shopping cart process: -1. The customer may add a product to the shopping cart only after opening it. -2. When selecting and adding a product to the basket customer needs to provide the quantity chosen. The product price is calculated by the system based on the current price list. -3. The customer may remove a product with a given price from the cart. -4. The customer can confirm the shopping cart and start the order fulfilment process. -5. The customer may also cancel the shopping cart and reject all selected products. -6. After shopping cart confirmation or cancellation, the product can no longer be added or removed from the cart. - -Write the code that fulfils this logic. Remember that in Event Sourcing each business operation has to result with a new business fact (so event). Use events and entities defined in previous exercises. - -![events](./assets/events.jpg) - -There are two variations: -1. Immutable, with functional command handlers composition and entities as anemic data model: [Immutable/BusinessLogicTests.cs](./Immutable/BusinessLogicTests.cs). -2. Classical, mutable aggregates (rich domain model): [Mutable/BusinessLogicTests.cs](./Mutable/BusinessLogicTests.cs). -3. Mixed approach, mutable aggregates (rich domain model), returning events from methods, using immutable DTOs: [Mixed/BusinessLogicTests.cs](./Mixed/BusinessLogicTests.cs). - -Select your preferred approach (or all) to solve this use case using EventStoreDB. Fill appropriate `EventStoreClientExtensions` - -_**Note**: If needed update entities, events or test setup structure_ - -## Solutions - -Read more in my articles: -- [CQRS is simpler than you think with .NET 6 and C# 10](https://event-driven.io/en/cqrs_is_simpler_than_you_think_with_net6/?utm_source=event_sourcing_net_workshop) -- [How to register all CQRS handlers by convention](https://event-driven.io/en/how_to_register_all_mediatr_handlers_by_convention?utm_source=event_sourcing_net_workshop) diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Tools/EventStoreDBTest.cs b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Tools/EventStoreDBTest.cs deleted file mode 100644 index 3c9f351d7..000000000 --- a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/Tools/EventStoreDBTest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using EventStore.Client; - -namespace IntroductionToEventSourcing.BusinessLogic.Tools; - -public abstract class EventStoreDBTest: IDisposable -{ - protected readonly EventStoreClient EventStore; - - protected EventStoreDBTest() - { - EventStore = - new EventStoreClient(EventStoreClientSettings.Create("esdb://localhost:2113?tls=false")); - } - - public virtual void Dispose() - { - EventStore.Dispose(); - } -} diff --git a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/assets/events.jpg b/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/assets/events.jpg deleted file mode 100644 index 35522f580..000000000 Binary files a/Workshops/IntroductionToEventSourcing/Solved/09-BusinessLogic.EventStoreDB/assets/events.jpg and /dev/null differ