Skip to content

Commit

Permalink
Added example business logic using Decider with Marten
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Aug 25, 2023
1 parent ca6c6f3 commit 9d76b04
Show file tree
Hide file tree
Showing 12 changed files with 754 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace IntroductionToEventSourcing.BusinessLogic.Immutable;
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution1;

// ENTITY
public record ShoppingCart(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using IntroductionToEventSourcing.BusinessLogic.Tools;
using Xunit;

namespace IntroductionToEventSourcing.BusinessLogic.Immutable;
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution1;

// EVENTS
public record ShoppingCartOpened(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Marten;

namespace IntroductionToEventSourcing.BusinessLogic.Immutable;
namespace IntroductionToEventSourcing.BusinessLogic.Immutable.Solution1;

public static class DocumentSessionExtensions
{
Expand Down
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);
}
}
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);
}
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
);
Loading

0 comments on commit 9d76b04

Please sign in to comment.