Skip to content

Commit

Permalink
Added slimmed business logic exercise
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed May 2, 2024
1 parent 7ca9108 commit 87480d9
Show file tree
Hide file tree
Showing 17 changed files with 1,206 additions and 0 deletions.
7 changes: 7 additions & 0 deletions EventSourcing.NetCore.sln
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>IntroductionToEventSourcing.BusinessLogic.Slimmed</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>



</Project>
@@ -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<ShoppingCartEvent>(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<InvalidOperationException>();

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<ShoppingCartEvent>(shoppingCartId);
events.Should().HaveCount(5);
events[0].Should().BeOfType<ShoppingCartOpened>();
events[1].Should().BeOfType<ProductItemAddedToShoppingCart>();
events[2].Should().BeOfType<ProductItemAddedToShoppingCart>();
events[3].Should().BeOfType<ProductItemRemovedFromShoppingCart>();
events[4].Should().BeOfType<ShoppingCartConfirmed>();
}
}
@@ -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);
}
}
@@ -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
}

0 comments on commit 87480d9

Please sign in to comment.