From d83b85bf8b9db1efbf7b7bca1572349b9ad4bf5c Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Fri, 26 Apr 2024 10:16:52 +0200 Subject: [PATCH] Updated exercises on events definitions and getting state to include union type for events --- .../Immutable/GettingStateFromEventsTests.cs | 51 ++++---- .../Mutable/GettingStateFromEventsTests.cs | 59 +++++---- .../Solved/01-EventsDefinition/README.md | 3 + .../{ => Solution1}/EventsDefinitionTests.cs | 2 +- .../Solution2/EventsDefinitionTests.cs | 119 ++++++++++++++++++ .../Solution1/GettingStateFromEventsTests.cs | 67 +++++----- .../Solution2/GettingStateFromEventsTests.cs | 73 +++++------ .../Mutable/GettingStateFromEventsTests.cs | 63 +++++----- 8 files changed, 293 insertions(+), 144 deletions(-) rename Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/{ => Solution1}/EventsDefinitionTests.cs (97%) create mode 100644 Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution2/EventsDefinitionTests.cs diff --git a/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Immutable/GettingStateFromEventsTests.cs b/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Immutable/GettingStateFromEventsTests.cs index 2e228937a..8da397250 100644 --- a/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Immutable/GettingStateFromEventsTests.cs +++ b/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Immutable/GettingStateFromEventsTests.cs @@ -2,32 +2,39 @@ using Xunit; namespace IntroductionToEventSourcing.GettingStateFromEvents.Immutable; +using static ShoppingCartEvent; // EVENTS -public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId -); +public abstract record ShoppingCartEvent +{ + public record ShoppingCartOpened( + Guid ShoppingCartId, + Guid ClientId + ): ShoppingCartEvent; -public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); + public record ProductItemAddedToShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem + ): ShoppingCartEvent; -public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); + public record ProductItemRemovedFromShoppingCart( + Guid ShoppingCartId, + PricedProductItem ProductItem + ): ShoppingCartEvent; -public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt -); + public record ShoppingCartConfirmed( + Guid ShoppingCartId, + DateTime ConfirmedAt + ): ShoppingCartEvent; -public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt -); + public record ShoppingCartCanceled( + Guid ShoppingCartId, + DateTime CanceledAt + ): ShoppingCartEvent; + + // This won't allow + private ShoppingCartEvent(){} +} // VALUE OBJECTS public record PricedProductItem( @@ -55,7 +62,7 @@ public enum ShoppingCartStatus public class GettingStateFromEventsTests { - private static ShoppingCart GetShoppingCart(IEnumerable events) + private static ShoppingCart GetShoppingCart(IEnumerable events) { // 1. Add logic here throw new NotImplementedException(); @@ -73,7 +80,7 @@ public void GettingState_ForSequenceOfEvents_ShouldSucceed() var pairOfShoes = new PricedProductItem(shoesId, 1, 100); var tShirt = new PricedProductItem(tShirtId, 1, 50); - var events = new object[] + var events = new ShoppingCartEvent[] { new ShoppingCartOpened(shoppingCartId, clientId), new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), diff --git a/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs b/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs index c9d93d659..e060c54f4 100644 --- a/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs +++ b/Workshops/IntroductionToEventSourcing/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs @@ -2,32 +2,39 @@ using Xunit; namespace IntroductionToEventSourcing.GettingStateFromEvents.Mutable; +using static ShoppingCartEvent; // EVENTS -public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId -); - -public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt -); - -public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt -); +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 + private ShoppingCartEvent(){} +} // VALUE OBJECTS public class PricedProductItem @@ -60,7 +67,7 @@ public enum ShoppingCartStatus public class GettingStateFromEventsTests { - private static ShoppingCart GetShoppingCart(IEnumerable events) + private static ShoppingCart GetShoppingCart(IEnumerable events) { // 1. Add logic here throw new NotImplementedException(); @@ -90,7 +97,7 @@ public void GettingState_ForSequenceOfEvents_ShouldSucceed() ProductId = tShirtId, Quantity = 1, UnitPrice = 50 }; - var events = new object[] + var events = new ShoppingCartEvent[] { new ShoppingCartOpened(shoppingCartId, clientId), new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), diff --git a/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/README.md b/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/README.md index 03c41cc1e..0e5a3e9ac 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/README.md +++ b/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/README.md @@ -16,3 +16,6 @@ Create sample events that represent a specific shopping cart. You can do that in Events model: ![events](./assets/events.jpg) + +1. Simple events structure [Solution1/EventsDefinitionTests.cs](./Solution1/EventsDefinitionTests.cs). +2. Events structure with union type to give option to tell that Shopping Cart event is one of the defined types [Solution2/EventsDefinitionTests.cs](./Solution2/EventsDefinitionTests.cs). Read more in [Union types in C#](https://event-driven.io/en/union_types_in_csharp/). diff --git a/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/EventsDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution1/EventsDefinitionTests.cs similarity index 97% rename from Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/EventsDefinitionTests.cs rename to Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution1/EventsDefinitionTests.cs index 325e9ba0e..5831be7e5 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/EventsDefinitionTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution1/EventsDefinitionTests.cs @@ -1,7 +1,7 @@ using FluentAssertions; using Xunit; -namespace IntroductionToEventSourcing.EventsDefinition; +namespace IntroductionToEventSourcing.EventsDefinition.Solution1; // 1. Define your events and entity here diff --git a/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution2/EventsDefinitionTests.cs b/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution2/EventsDefinitionTests.cs new file mode 100644 index 000000000..54f989014 --- /dev/null +++ b/Workshops/IntroductionToEventSourcing/Solved/01-EventsDefinition/Solution2/EventsDefinitionTests.cs @@ -0,0 +1,119 @@ +using FluentAssertions; +using Xunit; + +namespace IntroductionToEventSourcing.EventsDefinition.Solution2; +using static ShoppingCartEvent; + +// 1. Define your events and entity here + +// 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 + 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 record ImmutablePricedProductItem( + Guid ProductId, + int Quantity, + decimal UnitPrice +) +{ + public decimal TotalPrice => Quantity * UnitPrice; +} + +// ENTITY + +// regular one +public class ShoppingCart +{ + public Guid Id { get; set; } + public Guid ClientId { get; set; } + public ShoppingCartStatus Status { get; set; } + public IList ProductItems { get; set; } = new List(); + public DateTime? ConfirmedAt { get; set; } + public DateTime? CanceledAt { get; set; } +} + +// immutable one +public record ImmutableShoppingCart( + Guid Id, + Guid ClientId, + ShoppingCartStatus Status, + PricedProductItem[] ProductItems, + DateTime? ConfirmedAt = null, + DateTime? CanceledAt = null +); + +public enum ShoppingCartStatus +{ + Pending = 1, + Confirmed = 2, + Canceled = 4 +} + +public class EventsDefinitionTests +{ + [Fact] + public void AllEventTypes_ShouldBeDefined() + { + var shoppingCartId = Guid.NewGuid(); + var clientId = Guid.NewGuid(); + var pairOfShoes = new PricedProductItem + { + ProductId = Guid.NewGuid(), + Quantity = 1, + UnitPrice = 100 + }; + + var events = new object[] + { + new ShoppingCartOpened(shoppingCartId, clientId), + new ProductItemAddedToShoppingCart(shoppingCartId, pairOfShoes), + new ProductItemRemovedFromShoppingCart(shoppingCartId, pairOfShoes), + new ShoppingCartConfirmed(shoppingCartId, DateTime.UtcNow), + new ShoppingCartCanceled(shoppingCartId, DateTime.UtcNow) + }; + + const int expectedEventTypesCount = 5; + events.Should().HaveCount(expectedEventTypesCount); + events.GroupBy(e => e.GetType()).Should().HaveCount(expectedEventTypesCount); + } +} diff --git a/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution1/GettingStateFromEventsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution1/GettingStateFromEventsTests.cs index e22524e6e..42231aa27 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution1/GettingStateFromEventsTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution1/GettingStateFromEventsTests.cs @@ -2,32 +2,39 @@ using Xunit; namespace IntroductionToEventSourcing.GettingStateFromEvents.Immutable.Solution1; +using static ShoppingCartEvent; // EVENTS -public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId -); - -public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt -); - -public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt -); +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 + private ShoppingCartEvent(){} +} // VALUE OBJECTS public record PricedProductItem( @@ -60,7 +67,7 @@ public class GettingStateFromEventsTests /// /// /// - private static ShoppingCart GetShoppingCart(IEnumerable events) + private static ShoppingCart GetShoppingCart(IEnumerable events) { ShoppingCart shoppingCart = null!; @@ -73,7 +80,7 @@ private static ShoppingCart GetShoppingCart(IEnumerable events) opened.ShoppingCartId, opened.ClientId, ShoppingCartStatus.Pending, - Array.Empty() + [] ); break; case ProductItemAddedToShoppingCart productItemAdded: @@ -98,11 +105,7 @@ private static ShoppingCart GetShoppingCart(IEnumerable events) { ProductItems = shoppingCart.ProductItems .Select(pi => pi.ProductId == productItemRemoved.ProductItem.ProductId? - new PricedProductItem( - pi.ProductId, - pi.Quantity - productItemRemoved.ProductItem.Quantity, - pi.UnitPrice - ) + pi with { Quantity = pi.Quantity - productItemRemoved.ProductItem.Quantity } :pi ) .Where(pi => pi.Quantity > 0) @@ -140,7 +143,7 @@ public void GettingState_ForSequenceOfEvents_ShouldSucceed() var pairOfShoes = new PricedProductItem(shoesId, 1, 100); var tShirt = new PricedProductItem(tShirtId, 1, 50); - var events = new object[] + var events = new ShoppingCartEvent[] { new ShoppingCartOpened(shoppingCartId, clientId), new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), diff --git a/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution2/GettingStateFromEventsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution2/GettingStateFromEventsTests.cs index 31268d85e..331baf788 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution2/GettingStateFromEventsTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Immutable/Solution2/GettingStateFromEventsTests.cs @@ -2,32 +2,39 @@ using Xunit; namespace IntroductionToEventSourcing.GettingStateFromEvents.Immutable.Solution2; +using static ShoppingCartEvent; // EVENTS -public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId -); - -public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt -); - -public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt -); +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 + private ShoppingCartEvent(){} +} // VALUE OBJECTS public record PricedProductItem( @@ -46,10 +53,10 @@ public record ShoppingCart( DateTime? CanceledAt = null ) { - public static ShoppingCart Default() => - new (default, default, default, Array.Empty()); + public static ShoppingCart Initial() => + new (default, default, default, []); - public static ShoppingCart When(ShoppingCart shoppingCart, object @event) + public static ShoppingCart Evolve(ShoppingCart shoppingCart, ShoppingCartEvent @event) { return @event switch { @@ -81,11 +88,7 @@ shoppingCart with { ProductItems = shoppingCart.ProductItems .Select(pi => pi.ProductId == pricedProductItem.ProductId? - new PricedProductItem( - pi.ProductId, - pi.Quantity - pricedProductItem.Quantity, - pi.UnitPrice - ) + pi with { Quantity = pi.Quantity - pricedProductItem.Quantity } :pi ) .Where(pi => pi.Quantity > 0) @@ -122,8 +125,8 @@ public class GettingStateFromEventsTests /// /// /// - private static ShoppingCart GetShoppingCart(IEnumerable events) => - events.Aggregate(ShoppingCart.Default(), ShoppingCart.When); + private static ShoppingCart GetShoppingCart(IEnumerable events) => + events.Aggregate(ShoppingCart.Initial(), ShoppingCart.Evolve); [Fact] public void GettingState_ForSequenceOfEvents_ShouldSucceed() @@ -136,7 +139,7 @@ public void GettingState_ForSequenceOfEvents_ShouldSucceed() var pairOfShoes = new PricedProductItem(shoesId, 1, 100); var tShirt = new PricedProductItem(tShirtId, 1, 50); - var events = new object[] + var events = new ShoppingCartEvent[] { new ShoppingCartOpened(shoppingCartId, clientId), new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes), diff --git a/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs b/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs index c252e74a0..af4a96aca 100644 --- a/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs +++ b/Workshops/IntroductionToEventSourcing/Solved/02-GettingStateFromEvents/Mutable/GettingStateFromEventsTests.cs @@ -2,32 +2,39 @@ using Xunit; namespace IntroductionToEventSourcing.GettingStateFromEvents.Mutable; +using static ShoppingCartEvent; // EVENTS -public record ShoppingCartOpened( - Guid ShoppingCartId, - Guid ClientId -); - -public record ProductItemAddedToShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ProductItemRemovedFromShoppingCart( - Guid ShoppingCartId, - PricedProductItem ProductItem -); - -public record ShoppingCartConfirmed( - Guid ShoppingCartId, - DateTime ConfirmedAt -); - -public record ShoppingCartCanceled( - Guid ShoppingCartId, - DateTime CanceledAt -); +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 + private ShoppingCartEvent(){} +} // VALUE OBJECTS public class PricedProductItem @@ -50,7 +57,7 @@ public class ShoppingCart public DateTime? ConfirmedAt { get; private set; } public DateTime? CanceledAt { get; private set; } - public void When(object @event) + public void Evolve(object @event) { switch (@event) { @@ -138,13 +145,13 @@ public class GettingStateFromEventsTests /// /// /// - private static ShoppingCart GetShoppingCart(IEnumerable events) + private static ShoppingCart GetShoppingCart(IEnumerable events) { var shoppingCart = new ShoppingCart(); foreach (var @event in events) { - shoppingCart.When(@event); + shoppingCart.Evolve(@event); } return shoppingCart; @@ -173,7 +180,7 @@ public void GettingState_ForSequenceOfEvents_ShouldSucceed() ProductId = tShirtId, Quantity = 1, UnitPrice = 50 }; - var events = new object[] + var events = new ShoppingCartEvent[] { new ShoppingCartOpened(shoppingCartId, clientId), new ProductItemAddedToShoppingCart(shoppingCartId, twoPairsOfShoes),