Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added slimmed business logic exercise
- Loading branch information
1 parent
7ca9108
commit 87480d9
Showing
17 changed files
with
1,206 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
...hops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/07-BusinessLogic.Slimmed.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
108 changes: 108 additions & 0 deletions
108
...hops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/BusinessLogicTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>(); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...shops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/PricingCalculator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
136 changes: 136 additions & 0 deletions
136
Workshops/IntroductionToEventSourcing/07-BusinessLogic.Slimmed/Immutable/ShoppingCart.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.