-
-
Notifications
You must be signed in to change notification settings - Fork 512
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added example of using Marten with Decider pattern and Evolve function
- Loading branch information
1 parent
994d9ad
commit 850022e
Showing
6 changed files
with
338 additions
and
3 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
94 changes: 94 additions & 0 deletions
94
...nToEventSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution4/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,94 @@ | ||
using FluentAssertions; | ||
using IntroductionToEventSourcing.BusinessLogic.Tools; | ||
using Xunit; | ||
|
||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution4; | ||
|
||
using static ShoppingCart; | ||
using static ShoppingCartCommand; | ||
|
||
public class BusinessLogicTests: MartenTest | ||
{ | ||
[Fact] | ||
public async Task GettingState_ForSequenceOfEvents_ShouldSucceed() | ||
{ | ||
var shoppingCartId = ShoppingCartId.From(Guid.NewGuid()); | ||
var clientId = ClientId.From(Guid.NewGuid()); | ||
var shoesId = ProductId.From(Guid.NewGuid()); | ||
var tShirtId = ProductId.From(Guid.NewGuid()); | ||
|
||
var one = ProductQuantity.From(1); | ||
var two = ProductQuantity.From(2); | ||
|
||
var twoPairsOfShoes = new ProductItem(shoesId, two); | ||
var pairOfShoes = new ProductItem(shoesId, one); | ||
var tShirt = new ProductItem(tShirtId, one); | ||
|
||
var shoesPrice = ProductPrice.From(100); | ||
var tShirtPrice = ProductPrice.From(50); | ||
|
||
var pricedPairOfShoes = new PricedProductItem(shoesId, one, shoesPrice); | ||
var pricedTwoPairsOfShoes = new PricedProductItem(shoesId, two, shoesPrice); | ||
var pricedTShirt = new PricedProductItem(tShirtId, one, tShirtPrice); | ||
|
||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
new Open(shoppingCartId, clientId, DateTimeOffset.Now), | ||
CancellationToken.None | ||
); | ||
|
||
// Add two pairs of shoes | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
new AddProductItem(shoppingCartId, pricedTwoPairsOfShoes), | ||
CancellationToken.None | ||
); | ||
|
||
// Add T-Shirt | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
new AddProductItem(shoppingCartId, pricedTShirt), | ||
CancellationToken.None | ||
); | ||
|
||
// Remove pair of shoes | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
new RemoveProductItem(shoppingCartId, pricedPairOfShoes), | ||
CancellationToken.None | ||
); | ||
|
||
|
||
var pendingShoppingCart = | ||
await DocumentSession.Get<ShoppingCart>(shoppingCartId.Value, CancellationToken.None) as Pending; | ||
|
||
pendingShoppingCart.Should().NotBeNull(); | ||
pendingShoppingCart!.ProductItems.Should().HaveCount(3); | ||
|
||
pendingShoppingCart.ProductItems[0].Should() | ||
.Be((pricedTwoPairsOfShoes.ProductId, pricedTwoPairsOfShoes.Quantity.Value)); | ||
pendingShoppingCart.ProductItems[1].Should().Be((pricedTShirt.ProductId, pricedTShirt.Quantity.Value)); | ||
pendingShoppingCart.ProductItems[2].Should().Be((pairOfShoes.ProductId, -pairOfShoes.Quantity.Value)); | ||
|
||
// Confirm | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
new Confirm(shoppingCartId, DateTimeOffset.Now), | ||
CancellationToken.None | ||
); | ||
|
||
// Cancel | ||
var exception = await Record.ExceptionAsync(() => | ||
DocumentSession.Decide( | ||
shoppingCartId, | ||
new Cancel(shoppingCartId, DateTimeOffset.Now), | ||
CancellationToken.None | ||
) | ||
); | ||
exception.Should().BeOfType<InvalidOperationException>(); | ||
|
||
var shoppingCart = await DocumentSession.Get<ShoppingCart>(shoppingCartId.Value, CancellationToken.None); | ||
|
||
shoppingCart.Should().BeOfType<Closed>(); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...tSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution4/DocumentSessionExtensions.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,28 @@ | ||
using Marten; | ||
|
||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution4; | ||
|
||
public static class DocumentSessionExtensions | ||
{ | ||
public static async Task<TEntity> Get<TEntity>( | ||
this IDocumentSession session, | ||
Guid id, | ||
CancellationToken cancellationToken = default | ||
) where TEntity : class | ||
{ | ||
var entity = await session.Events.AggregateStreamAsync<TEntity>(id, token: cancellationToken); | ||
|
||
return entity ?? throw new InvalidOperationException($"Entity with id {id} was not found"); | ||
} | ||
|
||
public static Task Decide<TEntity, TCommand, TEvent>( | ||
this IDocumentSession session, | ||
Func<TCommand, TEntity, TEvent[]> decide, | ||
Func<TEntity> getDefault, | ||
Guid streamId, | ||
TCommand command, | ||
CancellationToken ct = default | ||
) where TEntity : class => | ||
session.Events.WriteToAggregate<TEntity>(streamId, stream => | ||
stream.AppendMany(decide(command, stream.Aggregate ?? getDefault()).Cast<object>().ToArray()), ct); | ||
} |
72 changes: 72 additions & 0 deletions
72
...ductionToEventSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution4/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,72 @@ | ||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution4; | ||
|
||
using static ShoppingCartEvent; | ||
|
||
public abstract record ShoppingCartEvent | ||
{ | ||
public record Opened(ClientId ClientId, DateTimeOffset OpenedAt): ShoppingCartEvent; | ||
|
||
public record ProductItemAdded(PricedProductItem ProductItem): ShoppingCartEvent; | ||
|
||
public record ProductItemRemoved(PricedProductItem ProductItem): ShoppingCartEvent; | ||
|
||
public record Confirmed(DateTimeOffset ConfirmedAt): ShoppingCartEvent; | ||
|
||
public record Canceled(DateTimeOffset CanceledAt): ShoppingCartEvent; | ||
} | ||
|
||
public record ShoppingCart | ||
{ | ||
public record Empty: ShoppingCart; | ||
|
||
public record Pending((ProductId ProductId, int Quantity)[] ProductItems): ShoppingCart | ||
{ | ||
public bool HasEnough(PricedProductItem productItem) => | ||
ProductItems | ||
.Where(pi => pi.ProductId == productItem.ProductId) | ||
.Sum(pi => pi.Quantity) >= productItem.Quantity.Value; | ||
|
||
public bool HasItems { get; } = | ||
ProductItems.Sum(pi => pi.Quantity) <= 0; | ||
} | ||
|
||
public record Closed: ShoppingCart; | ||
|
||
public ShoppingCart Apply(ShoppingCartEvent @event) => | ||
@event switch | ||
{ | ||
Opened => | ||
new Pending(Array.Empty<(ProductId ProductId, int Quantity)>()), | ||
|
||
ProductItemAdded (var (productId, quantity, _)) => | ||
this is Pending pending | ||
? pending with | ||
{ | ||
ProductItems = pending.ProductItems | ||
.Concat(new[] { (productId, quantity.Value) }) | ||
.ToArray() | ||
} | ||
: this, | ||
|
||
ProductItemRemoved (var (productId, quantity, _)) => | ||
this is Pending pending | ||
? pending with | ||
{ | ||
ProductItems = pending.ProductItems | ||
.Concat(new[] { (productId, -quantity.Value) }) | ||
.ToArray() | ||
} | ||
: this, | ||
|
||
Confirmed => | ||
this is Pending ? new Closed() : this, | ||
|
||
Canceled => | ||
this is Pending ? new Closed() : this, | ||
|
||
_ => this | ||
}; | ||
|
||
public Guid Id { get; set; } // Marten unfortunately forces you to have Id | ||
private ShoppingCart() { } // Not to allow inheritance | ||
} |
78 changes: 78 additions & 0 deletions
78
...ToEventSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution4/ShoppingCartDecider.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,78 @@ | ||
using Marten; | ||
|
||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution4; | ||
|
||
using static ShoppingCart; | ||
using static ShoppingCartEvent; | ||
using static ShoppingCartCommand; | ||
|
||
public abstract record ShoppingCartCommand | ||
{ | ||
public record Open(ShoppingCartId ShoppingCartId, ClientId ClientId, DateTimeOffset Now): ShoppingCartCommand; | ||
|
||
public record AddProductItem(ShoppingCartId ShoppingCartId, PricedProductItem ProductItem): ShoppingCartCommand; | ||
|
||
public record RemoveProductItem(ShoppingCartId ShoppingCartId, PricedProductItem ProductItem): ShoppingCartCommand; | ||
|
||
public record Confirm(ShoppingCartId ShoppingCartId, DateTimeOffset Now): ShoppingCartCommand; | ||
|
||
public record Cancel(ShoppingCartId ShoppingCartId, DateTimeOffset Now): ShoppingCartCommand; | ||
} | ||
|
||
// Value Objects | ||
public static class ShoppingCartService | ||
{ | ||
public static ShoppingCartEvent Decide( | ||
ShoppingCartCommand command, | ||
ShoppingCart state | ||
) => | ||
command switch | ||
{ | ||
Open open => Handle(open), | ||
AddProductItem addProduct => Handle(addProduct, state.EnsureIsPending()), | ||
RemoveProductItem removeProduct => Handle(removeProduct, state.EnsureIsPending()), | ||
Confirm confirm => Handle(confirm, state.EnsureIsPending()), | ||
Cancel cancel => Handle(cancel, state.EnsureIsPending()), | ||
_ => throw new InvalidOperationException($"Cannot handle {command.GetType().Name} command") | ||
}; | ||
|
||
private static Opened Handle(Open command) => | ||
new Opened(command.ClientId, command.Now); | ||
|
||
private static ProductItemAdded Handle(AddProductItem command, Pending shoppingCart) => | ||
new ProductItemAdded(command.ProductItem); | ||
|
||
private static ProductItemRemoved Handle(RemoveProductItem command, Pending shoppingCart) => | ||
shoppingCart.HasEnough(command.ProductItem) | ||
? new ProductItemRemoved(command.ProductItem) | ||
: throw new InvalidOperationException("Not enough product items to remove."); | ||
|
||
private static Confirmed Handle(Confirm command, Pending shoppingCart) => | ||
shoppingCart.HasItems | ||
? new Confirmed(DateTime.UtcNow) | ||
: throw new InvalidOperationException("Shopping cart is empty!"); | ||
|
||
private static Canceled Handle(Cancel command, Pending shoppingCart) => | ||
new Canceled(DateTime.UtcNow); | ||
|
||
private static Pending EnsureIsPending(this ShoppingCart shoppingCart) => | ||
shoppingCart as Pending ?? throw new InvalidOperationException( | ||
$"Invalid operation for '{shoppingCart.GetType().Name}' shopping card."); | ||
} | ||
|
||
public static class ShoppingCartDocumentSessionExtensions | ||
{ | ||
public static Task Decide( | ||
this IDocumentSession session, | ||
ShoppingCartId streamId, | ||
ShoppingCartCommand command, | ||
CancellationToken ct = default | ||
) => | ||
session.Decide<ShoppingCart, ShoppingCartCommand, ShoppingCartEvent>( | ||
(c, s) => new[] { ShoppingCartService.Decide(c, s) }, | ||
() => new Empty(), | ||
streamId.Value, | ||
command, | ||
ct | ||
); | ||
} |
64 changes: 64 additions & 0 deletions
64
...ntSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution4/ShoppingCartValueObjects.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,64 @@ | ||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution4; | ||
|
||
public record ProductItem( | ||
ProductId ProductId, | ||
ProductQuantity Quantity | ||
); | ||
|
||
public record PricedProductItem( | ||
ProductId ProductId, | ||
ProductQuantity Quantity, | ||
ProductPrice UnitPrice | ||
); | ||
|
||
public record ShoppingCartId(Guid Value) | ||
{ | ||
public static ShoppingCartId From(Guid? value) => | ||
(value != null && value != Guid.Empty) | ||
? new ShoppingCartId(value.Value) | ||
: throw new ArgumentOutOfRangeException(nameof(value)); | ||
} | ||
|
||
public record ClientId(Guid Value) | ||
{ | ||
public static ClientId From(Guid? value) => | ||
(value.HasValue && value != Guid.Empty) | ||
? new ClientId(value.Value) | ||
: throw new ArgumentOutOfRangeException(nameof(value)); | ||
} | ||
|
||
public record ProductId(Guid Value) | ||
{ | ||
public static ProductId From(Guid? value) => | ||
(value.HasValue && value != Guid.Empty) | ||
? new ProductId(value.Value) | ||
: throw new ArgumentOutOfRangeException(nameof(value)); | ||
} | ||
|
||
public record ProductQuantity(int Value): | ||
IComparable<ProductQuantity>, | ||
IComparable<int> | ||
{ | ||
public static ProductQuantity From(int? value) => | ||
value is > 0 | ||
? new ProductQuantity(value.Value) | ||
: throw new ArgumentOutOfRangeException(nameof(value)); | ||
|
||
public int CompareTo(ProductQuantity? other) | ||
{ | ||
if (ReferenceEquals(this, other)) return 0; | ||
if (ReferenceEquals(null, other)) return 1; | ||
return Value.CompareTo(other.Value); | ||
} | ||
|
||
public int CompareTo(int other) => | ||
Value.CompareTo(other); | ||
} | ||
|
||
public record ProductPrice(decimal Value) | ||
{ | ||
public static ProductPrice From(decimal? value) => | ||
value is > 0 | ||
? new ProductPrice(value.Value) | ||
: throw new ArgumentOutOfRangeException(nameof(value)); | ||
} |