diff --git a/EventSourcing.NetCore.sln b/EventSourcing.NetCore.sln
index ba2b5266f..4f7fdf5f6 100644
--- a/EventSourcing.NetCore.sln
+++ b/EventSourcing.NetCore.sln
@@ -465,6 +465,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "11-OptimisticConcurrency.Ev
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "11-OptimisticConcurrency.EventStoreDB.Tests", "Workshops\IntroductionToEventSourcing\11-OptimisticConcurrency.EventStoreDB.Tests\11-OptimisticConcurrency.EventStoreDB.Tests.csproj", "{37E05147-F579-458D-9B4F-25B54AC078DE}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "07-BusinessLogic.Slimmed", "Workshops\IntroductionToEventSourcing\07-BusinessLogic.Slimmed\07-BusinessLogic.Slimmed.csproj", "{B7DBA2CE-DAA9-42C0-A967-E4B62297EC1D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1043,6 +1045,10 @@ Global
{37E05147-F579-458D-9B4F-25B54AC078DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37E05147-F579-458D-9B4F-25B54AC078DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37E05147-F579-458D-9B4F-25B54AC078DE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B7DBA2CE-DAA9-42C0-A967-E4B62297EC1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B7DBA2CE-DAA9-42C0-A967-E4B62297EC1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B7DBA2CE-DAA9-42C0-A967-E4B62297EC1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B7DBA2CE-DAA9-42C0-A967-E4B62297EC1D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1234,6 +1240,7 @@ Global
{3AE580AB-209E-4F2A-AE7E-0BB89602A338} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{7829C11A-59AF-4C10-A679-312B8940A68D} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
{37E05147-F579-458D-9B4F-25B54AC078DE} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
+ {B7DBA2CE-DAA9-42C0-A967-E4B62297EC1D} = {14C7B928-9D6C-441A-8A1F-0C49173E73EB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/07-BusinessLogic.Slimmed.csproj b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/07-BusinessLogic.Slimmed.csproj
new file mode 100644
index 000000000..e944715a4
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/07-BusinessLogic.Slimmed.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ IntroductionToEventSourcing.BusinessLogic.Slimmed
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/BusinessLogicTests.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/BusinessLogicTests.cs
new file mode 100644
index 000000000..a2dda3494
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/BusinessLogicTests.cs
@@ -0,0 +1,108 @@
+using FluentAssertions;
+using IntroductionToEventSourcing.BusinessLogic.Slimmed.Tools;
+using Xunit;
+
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Immutable;
+
+using static ShoppingCartEvent;
+using static ShoppingCartCommand;
+
+public static class ShoppingCartExtensions
+{
+ public static ShoppingCart GetShoppingCart(this EventStore eventStore, Guid shoppingCartId) =>
+ eventStore.ReadStream(shoppingCartId).Aggregate(ShoppingCart.Default(), ShoppingCart.Evolve);
+}
+
+public class BusinessLogicTests
+{
+ [Fact]
+ public void RunningSequenceOfBusinessLogic_ShouldGenerateSequenceOfEvents()
+ {
+ 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);
+
+ var eventStore = new EventStore();
+
+ // Open
+ ShoppingCartEvent result =
+ ShoppingCartService.Handle(
+ new OpenShoppingCart(shoppingCartId, clientId)
+ );
+ eventStore.AppendToStream(shoppingCartId, (object[])[result]);
+
+ // Add Two Pair of Shoes
+ var shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = ShoppingCartService.Handle(
+ FakeProductPriceCalculator.Returning(shoesPrice),
+ new AddProductItemToShoppingCart(shoppingCartId, twoPairsOfShoes),
+ shoppingCart
+ );
+ eventStore.AppendToStream(shoppingCartId, (object[])[result]);
+
+ // Add T-Shirt
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = ShoppingCartService.Handle(
+ FakeProductPriceCalculator.Returning(tShirtPrice),
+ new AddProductItemToShoppingCart(shoppingCartId, tShirt),
+ shoppingCart
+ );
+ eventStore.AppendToStream(shoppingCartId, (object[])[result]);
+
+ // Remove a pair of shoes
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = ShoppingCartService.Handle(
+ new RemoveProductItemFromShoppingCart(shoppingCartId, pricedPairOfShoes),
+ shoppingCart
+ );
+ eventStore.AppendToStream(shoppingCartId, (object[])[result]);
+
+ // Confirm
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = ShoppingCartService.Handle(
+ new ConfirmShoppingCart(shoppingCartId),
+ shoppingCart
+ );
+ eventStore.AppendToStream(shoppingCartId, (object[])[result]);
+
+ // Try Cancel
+ var exception = Record.Exception(() =>
+ {
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = ShoppingCartService.Handle(
+ new CancelShoppingCart(shoppingCartId),
+ shoppingCart
+ );
+ eventStore.AppendToStream(shoppingCartId, (object[])[result]);
+ });
+ exception.Should().BeOfType();
+
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+
+ 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);
+
+ var events = eventStore.ReadStream(shoppingCartId);
+ events.Should().HaveCount(5);
+ events[0].Should().BeOfType();
+ events[1].Should().BeOfType();
+ events[2].Should().BeOfType();
+ events[3].Should().BeOfType();
+ events[4].Should().BeOfType();
+ }
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/PricingCalculator.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/PricingCalculator.cs
new file mode 100644
index 000000000..013d92548
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/PricingCalculator.cs
@@ -0,0 +1,24 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Immutable;
+
+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/07-BusinessLogic.Slimmed/Immutable/ShoppingCart.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/ShoppingCart.cs
new file mode 100644
index 000000000..300fa5219
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/ShoppingCart.cs
@@ -0,0 +1,136 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Immutable;
+using static ShoppingCartEvent;
+
+// 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 PricedProductItem(
+ Guid ProductId,
+ int Quantity,
+ decimal UnitPrice
+)
+{
+ public decimal TotalPrice => Quantity * UnitPrice;
+}
+
+public record ProductItem(Guid ProductId, int Quantity);
+
+
+// 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 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 static ShoppingCart Default() =>
+ new (default, default, default, []);
+
+ public static ShoppingCart Evolve(ShoppingCart shoppingCart, ShoppingCartEvent @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 enum ShoppingCartStatus
+{
+ Pending = 1,
+ Confirmed = 2,
+ Canceled = 4,
+
+ Closed = Confirmed | Canceled
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/ShoppingCartService.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/ShoppingCartService.cs
new file mode 100644
index 000000000..098ea10d8
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/ShoppingCartService.cs
@@ -0,0 +1,109 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Immutable;
+using static ShoppingCartEvent;
+using static ShoppingCartCommand;
+
+public abstract record ShoppingCartCommand
+{
+ public record OpenShoppingCart(
+ Guid ShoppingCartId,
+ Guid ClientId
+ ): ShoppingCartCommand;
+
+ public record AddProductItemToShoppingCart(
+ Guid ShoppingCartId,
+ ProductItem ProductItem
+ );
+
+ public record RemoveProductItemFromShoppingCart(
+ Guid ShoppingCartId,
+ PricedProductItem ProductItem
+ );
+
+ public record ConfirmShoppingCart(
+ Guid ShoppingCartId
+ );
+
+ public record CancelShoppingCart(
+ Guid ShoppingCartId
+ ): ShoppingCartCommand;
+
+ private ShoppingCartCommand() {}
+}
+
+public static class ShoppingCartService
+{
+ public static ShoppingCartOpened Handle(OpenShoppingCart command)
+ {
+ var (shoppingCartId, clientId) = command;
+
+ return new ShoppingCartOpened(
+ shoppingCartId,
+ clientId
+ );
+ }
+
+ public static ProductItemAddedToShoppingCart Handle(
+ IProductPriceCalculator priceCalculator,
+ 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 = priceCalculator.Calculate(productItem);
+
+ return new ProductItemAddedToShoppingCart(
+ cartId,
+ pricedProductItem
+ );
+ }
+
+ 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 static ShoppingCartConfirmed Handle(ConfirmShoppingCart command, ShoppingCart shoppingCart)
+ {
+ if (shoppingCart.IsClosed)
+ throw new InvalidOperationException($"Confirming cart in '{shoppingCart.Status}' status is not allowed.");
+
+ if(shoppingCart.ProductItems.Length == 0)
+ throw new InvalidOperationException($"Cannot confirm empty shopping cart");
+
+ return new ShoppingCartConfirmed(
+ shoppingCart.Id,
+ DateTime.UtcNow
+ );
+ }
+
+ 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
+ );
+ }
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/Aggregate.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/Aggregate.cs
new file mode 100644
index 000000000..8934d35ed
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/Aggregate.cs
@@ -0,0 +1,8 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Mixed;
+
+public interface IAggregate
+{
+ Guid Id { get; }
+
+ void Evolve(object @event) { }
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/BusinessLogicTests.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/BusinessLogicTests.cs
new file mode 100644
index 000000000..926c0d29e
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/BusinessLogicTests.cs
@@ -0,0 +1,98 @@
+using FluentAssertions;
+using IntroductionToEventSourcing.BusinessLogic.Slimmed.Tools;
+using Xunit;
+
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Mixed;
+
+using static ShoppingCartEvent;
+
+public static class ShoppingCartExtensions
+{
+ public static ShoppingCart GetShoppingCart(this EventStore eventStore, Guid shoppingCartId) =>
+ eventStore.ReadStream(shoppingCartId)
+ .Aggregate(ShoppingCart.Initial(), (shoppingCart, @event) =>
+ {
+ shoppingCart.Evolve(@event);
+ return shoppingCart;
+ });
+}
+
+public class BusinessLogicTests
+{
+ [Fact]
+ public void RunningSequenceOfBusinessLogic_ShouldGenerateSequenceOfEvents()
+ {
+ 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 };
+
+ var eventStore = new EventStore();
+
+ // Open
+ var (opened,_) = ShoppingCart.Open(shoppingCartId, clientId);
+ eventStore.AppendToStream(shoppingCartId, [opened]);
+
+ // Add Two Pair of Shoes
+ var shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ ShoppingCartEvent result = shoppingCart.AddProduct(
+ FakeProductPriceCalculator.Returning(shoesPrice),
+ twoPairsOfShoes
+ );
+ eventStore.AppendToStream(shoppingCartId, [result]);
+
+ // Add T-Shirt
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = shoppingCart.AddProduct(
+ FakeProductPriceCalculator.Returning(tShirtPrice),
+ tShirt
+ );
+ eventStore.AppendToStream(shoppingCartId, [result]);
+
+ // Remove a pair of shoes
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = shoppingCart.RemoveProduct(pricedPairOfShoes);
+ eventStore.AppendToStream(shoppingCartId, [result]);
+
+ // Confirm
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = shoppingCart.Confirm();
+ eventStore.AppendToStream(shoppingCartId, [result]);
+
+ // Try Cancel
+ var exception = Record.Exception(() =>
+ {
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+ result = shoppingCart.Cancel();
+ eventStore.AppendToStream(shoppingCartId, [result]);
+ });
+ exception.Should().BeOfType();
+
+ shoppingCart = eventStore.GetShoppingCart(shoppingCartId);
+
+ 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().BeEquivalentTo(pricedPairOfShoes);
+ shoppingCart.ProductItems[1].Should().BeEquivalentTo(pricedTShirt);
+
+ var events = eventStore.ReadStream(shoppingCartId);
+ events.Should().HaveCount(5);
+ events[0].Should().BeOfType();
+ events[1].Should().BeOfType();
+ events[2].Should().BeOfType();
+ events[3].Should().BeOfType();
+ events[4].Should().BeOfType();
+ }
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/PricingCalculator.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/PricingCalculator.cs
new file mode 100644
index 000000000..2ef3e96a5
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/PricingCalculator.cs
@@ -0,0 +1,21 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Mixed;
+
+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) =>
+ new() { ProductId = productItem.ProductId, Quantity = productItem.Quantity, UnitPrice = value };
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/ShoppingCart.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/ShoppingCart.cs
new file mode 100644
index 000000000..1360276ef
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mixed/ShoppingCart.cs
@@ -0,0 +1,236 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Mixed;
+
+using static ShoppingCartEvent;
+
+// 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; }
+}
+
+// ENTITY
+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 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, ShoppingCart) Open(
+ Guid cartId,
+ Guid clientId)
+ {
+ var @event = new ShoppingCartOpened(
+ cartId,
+ clientId
+ );
+
+ return (@event, new ShoppingCart(@event));
+ }
+
+ public static ShoppingCart Initial() => new();
+
+ 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)].Quantity += quantityToAdd;
+ }
+
+ 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)].Quantity -= quantityToRemove;
+ }
+
+ public ShoppingCartConfirmed Confirm()
+ {
+ if (IsClosed)
+ throw new InvalidOperationException(
+ $"Confirming cart in '{Status}' status is not allowed.");
+
+ if(ProductItems.Count == 0)
+ throw new InvalidOperationException($"Cannot confirm empty shopping cart");
+
+ 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
+}
diff --git a/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mutable/Aggregate.cs b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mutable/Aggregate.cs
new file mode 100644
index 000000000..12fcb73b0
--- /dev/null
+++ b/Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Mutable/Aggregate.cs
@@ -0,0 +1,24 @@
+namespace IntroductionToEventSourcing.BusinessLogic.Slimmed.Mutable;
+
+public abstract class Aggregate
+{
+ public Guid Id { get; protected set; } = default!;
+
+ private readonly Queue