-
-
Notifications
You must be signed in to change notification settings - Fork 514
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added example business logic using Decider with Marten
- Loading branch information
1 parent
ca6c6f3
commit 95de430
Showing
12 changed files
with
754 additions
and
3 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
...ssLogic.Marten/Immutable/BusinessLogic.cs → ...rten/Immutable/Solution1/BusinessLogic.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
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
2 changes: 1 addition & 1 deletion
2
...en/Immutable/DocumentSessionExtensions.cs → ...le/Solution1/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
88 changes: 88 additions & 0 deletions
88
...nToEventSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution2/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,88 @@ | ||
using FluentAssertions; | ||
using IntroductionToEventSourcing.BusinessLogic.Tools; | ||
using Marten; | ||
using Xunit; | ||
|
||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution2; | ||
|
||
using static ShoppingCartCommand; | ||
|
||
// Business logic | ||
|
||
public class BusinessLogicTests: MartenTest | ||
{ | ||
[Fact] | ||
public async Task GettingState_ForSequenceOfEvents_ShouldSucceed() | ||
{ | ||
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 pricedTwoPairsOfShoes = new PricedProductItem(shoesId, 2, shoesPrice); | ||
var pricedTShirt = new PricedProductItem(tShirtId, 1, tShirtPrice); | ||
|
||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
OpenShoppingCart.From(shoppingCartId, clientId), | ||
CancellationToken.None | ||
); | ||
|
||
// Add two pairs of shoes | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
AddProductItemToShoppingCart.From(shoppingCartId, pricedTwoPairsOfShoes), | ||
CancellationToken.None | ||
); | ||
|
||
// Add T-Shirt | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
AddProductItemToShoppingCart.From(shoppingCartId, pricedTShirt), | ||
CancellationToken.None | ||
); | ||
|
||
// Remove pair of shoes | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
RemoveProductItemFromShoppingCart.From(shoppingCartId, pricedPairOfShoes), | ||
CancellationToken.None | ||
); | ||
|
||
// Confirm | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
ConfirmShoppingCart.From(shoppingCartId), | ||
CancellationToken.None | ||
); | ||
|
||
// Cancel | ||
var exception = await Record.ExceptionAsync(async () => | ||
{ | ||
await DocumentSession.Decide( | ||
shoppingCartId, | ||
CancelShoppingCart.From(shoppingCartId), | ||
CancellationToken.None | ||
); | ||
} | ||
); | ||
exception.Should().BeOfType<InvalidOperationException>(); | ||
|
||
var shoppingCart = await DocumentSession.Get<ShoppingCart>(shoppingCartId, CancellationToken.None); | ||
|
||
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); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
...tSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution2/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,27 @@ | ||
using Marten; | ||
|
||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution2; | ||
|
||
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, | ||
Guid streamId, | ||
TCommand command, | ||
CancellationToken ct = default | ||
) where TEntity : class => | ||
session.Events.WriteToAggregate<TEntity>(streamId, stream => | ||
stream.AppendMany(decide(command, stream.Aggregate).Cast<object>().ToArray()), ct); | ||
} |
129 changes: 129 additions & 0 deletions
129
...ductionToEventSourcing/Solved/08-BusinessLogic.Marten/Immutable/Solution2/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,129 @@ | ||
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution2; | ||
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; | ||
} | ||
|
||
// 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 static ShoppingCart Create(ShoppingCartOpened opened) => | ||
new ShoppingCart( | ||
opened.ShoppingCartId, | ||
opened.ClientId, | ||
ShoppingCartStatus.Pending, | ||
Array.Empty<PricedProductItem>() | ||
); | ||
|
||
public ShoppingCart Apply(ProductItemAddedToShoppingCart productItemAdded) => | ||
this with | ||
{ | ||
ProductItems = ProductItems | ||
.Concat(new[] { productItemAdded.ProductItem }) | ||
.GroupBy(pi => pi.ProductId) | ||
.Select(group => group.Count() == 1 | ||
? group.First() | ||
: new PricedProductItem( | ||
group.Key, | ||
group.Sum(pi => pi.Quantity), | ||
group.First().UnitPrice | ||
) | ||
) | ||
.ToArray() | ||
}; | ||
|
||
public ShoppingCart Apply(ProductItemRemovedFromShoppingCart productItemRemoved) => | ||
this with | ||
{ | ||
ProductItems = ProductItems | ||
.Select(pi => pi.ProductId == productItemRemoved.ProductItem.ProductId | ||
? new PricedProductItem( | ||
pi.ProductId, | ||
pi.Quantity - productItemRemoved.ProductItem.Quantity, | ||
pi.UnitPrice | ||
) | ||
: pi | ||
) | ||
.Where(pi => pi.Quantity > 0) | ||
.ToArray() | ||
}; | ||
|
||
public ShoppingCart Apply(ShoppingCartConfirmed confirmed) => | ||
this with | ||
{ | ||
Status = ShoppingCartStatus.Confirmed, | ||
ConfirmedAt = confirmed.ConfirmedAt | ||
}; | ||
|
||
public ShoppingCart Apply(ShoppingCartCanceled canceled) => | ||
this with | ||
{ | ||
Status = ShoppingCartStatus.Canceled, | ||
CanceledAt = canceled.CanceledAt | ||
}; | ||
|
||
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 enum ShoppingCartStatus | ||
{ | ||
Pending = 1, | ||
Confirmed = 2, | ||
Canceled = 4, | ||
|
||
Closed = Confirmed | Canceled | ||
} | ||
|
||
// VALUE OBJECTS | ||
public record ProductItem( | ||
Guid ProductId, | ||
int Quantity | ||
); | ||
|
||
public record PricedProductItem( | ||
Guid ProductId, | ||
int Quantity, | ||
decimal UnitPrice | ||
); |
Oops, something went wrong.