diff --git a/Sample/ECommerce/CartsMinimalApi/Carts.Api/Carts.Api.csproj b/Sample/ECommerce/CartsMinimalApi/Carts.Api/Carts.Api.csproj
new file mode 100644
index 000000000..51c65fac0
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts.Api/Carts.Api.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts.Api/Program.cs b/Sample/ECommerce/CartsMinimalApi/Carts.Api/Program.cs
new file mode 100644
index 000000000..1c09aa229
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts.Api/Program.cs
@@ -0,0 +1,9 @@
+using Carter;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddCarter();
+
+var app = builder.Build();
+app.MapCarter();
+
+app.Run();
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartModule.cs b/Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartModule.cs
new file mode 100644
index 000000000..d70b91468
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartModule.cs
@@ -0,0 +1,53 @@
+using Carter;
+using Carter.Request;
+using Carts.ShoppingCarts;
+using Carts.ShoppingCarts.AddingProduct;
+using Carts.ShoppingCarts.CancellingCart;
+using Carts.ShoppingCarts.ConfirmingCart;
+using Carts.ShoppingCarts.OpeningCart;
+using Carts.ShoppingCarts.RemovingProduct;
+
+public class ShoppingCartModule: ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapPost("/shoppingcart", OpenCart);
+ app.MapPost("/shoppingcart/{cartId:guid}/products", AddProduct);
+ app.MapDelete("/shoppingcart/{cartId:guid}/products/{productId:guid}", RemoveProduct);
+ app.MapPut("/shoppingcart/{cartId:guid}/confirmation", ConfirmCart);
+ app.MapDelete("/shoppingcart/{cartId:guid}", CancelCart);
+ }
+
+ private IResult CancelCart(HttpContext context, Guid cartId, ICancelCartService cancelCartService)
+ {
+ cancelCartService.CancelCart(cartId);
+ return Results.StatusCode(204);
+ }
+
+ private IResult ConfirmCart(HttpContext context, Guid cartId, IConfirmCartService confirmCartService)
+ {
+ confirmCartService.Confirm(cartId);
+ return Results.StatusCode(204);
+ }
+
+ private IResult RemoveProduct(HttpContext context, Guid cartId, Guid productId,
+ IRemoveProductService removeProductService)
+ {
+ removeProductService.RemoveProduct(cartId, productId, context.Request.Query.As("quantity"),
+ context.Request.Query.As("unitPrice"));
+ return Results.StatusCode(204);
+ }
+
+ private IResult AddProduct(HttpContext context, Guid cartId, AddProductRequest model,
+ IAddProductService addProductService)
+ {
+ addProductService.AddProduct(cartId, model.ProductId, model.Quantity);
+ return Results.StatusCode(204);
+ }
+
+ private IResult OpenCart(HttpContext context, OpenShoppingCartRequest model, IOpenCartService openCartService)
+ {
+ var cartId = openCartService.OpenCart(model.ClientId);
+ return Results.Created($"/shoppingcart/{cartId}", null);
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartsRequests.cs b/Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartsRequests.cs
new file mode 100644
index 000000000..afae67983
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts.Api/ShoppingCartsRequests.cs
@@ -0,0 +1,27 @@
+namespace Carts.ShoppingCarts;
+
+public record OpenShoppingCartRequest(
+ Guid ClientId
+);
+
+public record AddProductRequest(
+ Guid ProductId,
+ int Quantity
+);
+
+public record PricedProductItemRequest(
+ Guid? ProductId,
+ int? Quantity,
+ decimal? UnitPrice
+);
+
+public record RemoveProductRequest(
+ PricedProductItemRequest? ProductItem
+);
+
+public record ConfirmShoppingCartRequest;
+
+public record GetCartAtVersionRequest(
+ Guid? CartId,
+ long? Version
+);
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.Development.json b/Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.json b/Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts.Api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/Carts.csproj b/Sample/ECommerce/CartsMinimalApi/Carts/Carts.csproj
new file mode 100644
index 000000000..aefdfc0bd
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/Carts.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/DataAccess/IShoppingCartRepository.cs b/Sample/ECommerce/CartsMinimalApi/Carts/DataAccess/IShoppingCartRepository.cs
new file mode 100644
index 000000000..9b90cd2d6
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/DataAccess/IShoppingCartRepository.cs
@@ -0,0 +1,7 @@
+namespace Carts.DataAccess;
+
+public interface IShoppingCartRepository
+{
+ ShoppingCart GetById(Guid id);
+ void Save(ShoppingCart cart);
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/Pricing/IProductPriceCalculator.cs b/Sample/ECommerce/CartsMinimalApi/Carts/Pricing/IProductPriceCalculator.cs
new file mode 100644
index 000000000..6429af0fd
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/Pricing/IProductPriceCalculator.cs
@@ -0,0 +1,8 @@
+using Carts.ShoppingCarts.Products;
+
+namespace Carts.Pricing;
+
+public interface IProductPriceCalculator
+{
+ IReadOnlyList Calculate(params ProductItem[] productItems);
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/Pricing/RandomProductPriceCalculator.cs b/Sample/ECommerce/CartsMinimalApi/Carts/Pricing/RandomProductPriceCalculator.cs
new file mode 100644
index 000000000..143aa602b
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/Pricing/RandomProductPriceCalculator.cs
@@ -0,0 +1,19 @@
+using Carts.ShoppingCarts.Products;
+
+namespace Carts.Pricing;
+
+public class RandomProductPriceCalculator: IProductPriceCalculator
+{
+ public IReadOnlyList Calculate(params ProductItem[] productItems)
+ {
+ if (productItems.Length == 0)
+ throw new ArgumentOutOfRangeException(nameof(productItems.Length));
+
+ var random = new Random();
+
+ return productItems
+ .Select(pi =>
+ PricedProductItem.Create(pi, Math.Round(new decimal(random.NextDouble() * 100), 2)))
+ .ToList();
+ }
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCart.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCart.cs
new file mode 100644
index 000000000..009dd9d9e
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCart.cs
@@ -0,0 +1,114 @@
+using Carts.Pricing;
+using Carts.ShoppingCarts.Products;
+using Core.Extensions;
+
+namespace Carts;
+
+public class ShoppingCart
+{
+ public Guid Id { get; private set; }
+
+ public Guid ClientId { get; private set; }
+
+ public ShoppingCartStatus Status { get; private set; }
+
+ public IList ProductItems { get; private set; } = default!;
+
+ public Money TotalPrice => ProductItems.Sum(pi => pi.TotalPrice);
+
+ public static ShoppingCart Open(
+ Guid cartId,
+ Guid clientId)
+ {
+ return new ShoppingCart(cartId, clientId);
+ }
+
+ public ShoppingCart() { }
+
+ private ShoppingCart(
+ Guid id,
+ Guid clientId)
+ {
+ Id = id;
+ ClientId = clientId;
+ ProductItems = new List();
+ Status = ShoppingCartStatus.Pending;
+ }
+
+ public void AddProduct(
+ IProductPriceCalculator productPriceCalculator,
+ ProductItem productItem)
+ {
+ if (Status != ShoppingCartStatus.Pending)
+ throw new InvalidOperationException($"Adding product for the cart in '{Status}' status is not allowed.");
+
+ var pricedProductItem = productPriceCalculator.Calculate(productItem).Single();
+
+ var existingProductItem = FindProductItemMatchingWith(pricedProductItem);
+
+ if (existingProductItem is null)
+ {
+ ProductItems.Add(pricedProductItem);
+ return;
+ }
+
+ ProductItems.Replace(
+ existingProductItem,
+ existingProductItem.MergeWith(pricedProductItem)
+ );
+ }
+
+
+ public void RemoveProduct(
+ PricedProductItem productItemToBeRemoved)
+ {
+ if (Status != ShoppingCartStatus.Pending)
+ throw new InvalidOperationException($"Removing product from the cart in '{Status}' status is not allowed.");
+
+ var existingProductItem = FindProductItemMatchingWith(productItemToBeRemoved);
+
+ if (existingProductItem is null)
+ throw new InvalidOperationException(
+ $"Product with id `{productItemToBeRemoved.ProductId}` and price '{productItemToBeRemoved.UnitPrice}' was not found in cart.");
+
+ if (!existingProductItem.HasEnough(productItemToBeRemoved.Quantity))
+ throw new InvalidOperationException(
+ $"Cannot remove {productItemToBeRemoved.Quantity} items of Product with id `{productItemToBeRemoved.ProductId}` as there are only ${existingProductItem.Quantity} items in card");
+
+
+ if (existingProductItem.HasTheSameQuantity(productItemToBeRemoved))
+ {
+ ProductItems.Remove(existingProductItem);
+ return;
+ }
+
+ ProductItems.Replace(
+ existingProductItem,
+ existingProductItem.Subtract(productItemToBeRemoved)
+ );
+ }
+
+ public void Confirm()
+ {
+ if (Status != ShoppingCartStatus.Pending)
+ throw new InvalidOperationException($"Confirming cart in '{Status}' status is not allowed.");
+
+ Status = ShoppingCartStatus.Confirmed;
+ }
+
+
+ public void Cancel()
+ {
+ if (Status != ShoppingCartStatus.Pending)
+ throw new InvalidOperationException($"Canceling cart in '{Status}' status is not allowed.");
+
+
+ Status = ShoppingCartStatus.Canceled;
+ }
+
+ private PricedProductItem? FindProductItemMatchingWith(PricedProductItem productItem)
+ {
+ return ProductItems
+ .SingleOrDefault(pi => pi.MatchesProductAndPrice(productItem));
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCartStatus.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCartStatus.cs
new file mode 100644
index 000000000..9fc7429f4
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCartStatus.cs
@@ -0,0 +1,8 @@
+namespace Carts;
+
+public enum ShoppingCartStatus
+{
+ Pending = 1,
+ Confirmed = 2,
+ Canceled = 4
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/AddingProduct/AddProductService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/AddingProduct/AddProductService.cs
new file mode 100644
index 000000000..37b20c9df
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/AddingProduct/AddProductService.cs
@@ -0,0 +1,25 @@
+using Carts.DataAccess;
+using Carts.Pricing;
+using Carts.ShoppingCarts.Products;
+
+namespace Carts.ShoppingCarts.AddingProduct;
+
+public class AddProductService: IAddProductService
+{
+ private readonly IProductPriceCalculator productPriceCalculator;
+ private readonly IShoppingCartRepository shoppingCartRepository;
+
+ public AddProductService(IProductPriceCalculator productPriceCalculator,
+ IShoppingCartRepository shoppingCartRepository)
+ {
+ this.productPriceCalculator = productPriceCalculator;
+ this.shoppingCartRepository = shoppingCartRepository;
+ }
+
+ public void AddProduct(Guid cartId, Guid productId, int quantity)
+ {
+ var cart = this.shoppingCartRepository.GetById(cartId);
+ cart.AddProduct(this.productPriceCalculator, ProductItem.Create(productId, quantity));
+ this.shoppingCartRepository.Save(cart);
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/AddingProduct/IAddProductService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/AddingProduct/IAddProductService.cs
new file mode 100644
index 000000000..108ae822f
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/AddingProduct/IAddProductService.cs
@@ -0,0 +1,6 @@
+namespace Carts.ShoppingCarts.AddingProduct;
+
+public interface IAddProductService
+{
+ void AddProduct(Guid cartId, Guid productId, int quantity);
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/CancellingCart/CancelCartService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/CancellingCart/CancelCartService.cs
new file mode 100644
index 000000000..71caadbef
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/CancellingCart/CancelCartService.cs
@@ -0,0 +1,20 @@
+using Carts.DataAccess;
+
+namespace Carts.ShoppingCarts.CancellingCart;
+
+public class CancelCartService: ICancelCartService
+{
+ private readonly IShoppingCartRepository shoppingCartRepository;
+
+ public CancelCartService(IShoppingCartRepository shoppingCartRepository)
+ {
+ this.shoppingCartRepository = shoppingCartRepository;
+ }
+
+ public void CancelCart(Guid cartId)
+ {
+ var cart = this.shoppingCartRepository.GetById(cartId);
+ cart.Cancel();
+ this.shoppingCartRepository.Save(cart);
+ }
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/CancellingCart/ICancelCartService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/CancellingCart/ICancelCartService.cs
new file mode 100644
index 000000000..bcbce6847
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/CancellingCart/ICancelCartService.cs
@@ -0,0 +1,6 @@
+namespace Carts.ShoppingCarts.CancellingCart;
+
+public interface ICancelCartService
+{
+ void CancelCart(Guid cartId);
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/ConfirmingCart/ConfirmCartService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/ConfirmingCart/ConfirmCartService.cs
new file mode 100644
index 000000000..ad266c27b
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/ConfirmingCart/ConfirmCartService.cs
@@ -0,0 +1,20 @@
+using Carts.DataAccess;
+
+namespace Carts.ShoppingCarts.ConfirmingCart;
+
+public class ConfirmCartService: IConfirmCartService
+{
+ private readonly IShoppingCartRepository shoppingCartRepository;
+
+ public ConfirmCartService(IShoppingCartRepository shoppingCartRepository)
+ {
+ this.shoppingCartRepository = shoppingCartRepository;
+ }
+
+ public void Confirm(Guid cartId)
+ {
+ var cart = this.shoppingCartRepository.GetById(cartId);
+ cart.Confirm();
+ this.shoppingCartRepository.Save(cart);
+ }
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/ConfirmingCart/IConfirmCartService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/ConfirmingCart/IConfirmCartService.cs
new file mode 100644
index 000000000..8ef7925f8
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/ConfirmingCart/IConfirmCartService.cs
@@ -0,0 +1,6 @@
+namespace Carts.ShoppingCarts.ConfirmingCart;
+
+public interface IConfirmCartService
+{
+ void Confirm(Guid cartId);
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/OpeningCart/IOpenCartService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/OpeningCart/IOpenCartService.cs
new file mode 100644
index 000000000..5583091c9
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/OpeningCart/IOpenCartService.cs
@@ -0,0 +1,6 @@
+namespace Carts.ShoppingCarts.OpeningCart;
+
+public interface IOpenCartService
+{
+ Guid OpenCart(Guid clientId);
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/OpeningCart/OpenCartService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/OpeningCart/OpenCartService.cs
new file mode 100644
index 000000000..9959c12f7
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/OpeningCart/OpenCartService.cs
@@ -0,0 +1,20 @@
+using Carts.DataAccess;
+
+namespace Carts.ShoppingCarts.OpeningCart;
+
+public class OpenCartService: IOpenCartService
+{
+ private readonly IShoppingCartRepository shoppingCartRepository;
+
+ public OpenCartService(IShoppingCartRepository shoppingCartRepository)
+ {
+ this.shoppingCartRepository = shoppingCartRepository;
+ }
+
+ public Guid OpenCart(Guid clientId)
+ {
+ var cart = ShoppingCart.Open(Guid.NewGuid(), clientId);
+ this.shoppingCartRepository.Save(cart);
+ return cart.Id;
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/Money.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/Money.cs
new file mode 100644
index 000000000..9cfa74323
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/Money.cs
@@ -0,0 +1,53 @@
+namespace Carts.ShoppingCarts.Products;
+
+public struct Money: IEquatable
+{
+ private decimal Value { get; }
+
+ public Money(decimal value)
+ {
+ this.Value = value;
+
+ }
+
+ public bool Equals(Money other)
+ {
+ return Value == other.Value;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj))
+ return false;
+
+ return obj is Money money && Equals(money);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return ((Value.GetHashCode()) * 397);
+ }
+ }
+
+ public static bool operator ==(Money a, Money b)
+ {
+ return a.Equals(b);
+ }
+
+ public static bool operator !=(Money a, Money b)
+ {
+ return !(a == b);
+ }
+
+ public static Money operator *(Money a, int quantity)
+ {
+ return new Money(a.Value * quantity);
+ }
+
+ public static Money operator +(Money a, Money b)
+ {
+ return new Money(a.Value + b.Value);
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/PricedProductItem.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/PricedProductItem.cs
new file mode 100644
index 000000000..1766ceba9
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/PricedProductItem.cs
@@ -0,0 +1,69 @@
+namespace Carts.ShoppingCarts.Products;
+
+public class PricedProductItem
+{
+ public Guid ProductId => ProductItem.ProductId;
+
+ public int Quantity => ProductItem.Quantity;
+
+ public Money UnitPrice { get; }
+
+ public Money TotalPrice => UnitPrice * Quantity;
+ public ProductItem ProductItem { get; }
+
+ private PricedProductItem(ProductItem productItem, Money unitPrice)
+ {
+ ProductItem = productItem;
+ UnitPrice = unitPrice;
+ }
+
+ public static PricedProductItem Create(Guid? productId, int? quantity, decimal? unitPrice)
+ {
+ return Create(
+ ProductItem.Create(productId, quantity),
+ unitPrice
+ );
+ }
+
+ public static PricedProductItem Create(ProductItem productItem, decimal? unitPrice)
+ {
+ return unitPrice switch
+ {
+ null => throw new ArgumentNullException(nameof(unitPrice)),
+ <= 0 => throw new ArgumentOutOfRangeException(nameof(unitPrice),
+ "Unit price has to be positive number"),
+ _ => new PricedProductItem(productItem, new Money(unitPrice.Value))
+ };
+ }
+
+ public bool MatchesProductAndPrice(PricedProductItem pricedProductItem)
+ {
+ return ProductId == pricedProductItem.ProductId && UnitPrice == pricedProductItem.UnitPrice;
+ }
+
+ public PricedProductItem MergeWith(PricedProductItem pricedProductItem)
+ {
+ if (!MatchesProductAndPrice(pricedProductItem))
+ throw new ArgumentException("Product or price does not match.");
+
+ return new PricedProductItem(ProductItem.MergeWith(pricedProductItem.ProductItem), UnitPrice);
+ }
+
+ public PricedProductItem Subtract(PricedProductItem pricedProductItem)
+ {
+ if (!MatchesProductAndPrice(pricedProductItem))
+ throw new ArgumentException("Product or price does not match.");
+
+ return new PricedProductItem(ProductItem.Substract(pricedProductItem.ProductItem), UnitPrice);
+ }
+
+ public bool HasEnough(int quantity)
+ {
+ return ProductItem.HasEnough(quantity);
+ }
+
+ public bool HasTheSameQuantity(PricedProductItem pricedProductItem)
+ {
+ return ProductItem.HasTheSameQuantity(pricedProductItem.ProductItem);
+ }
+}
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/ProductItem.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/ProductItem.cs
new file mode 100644
index 000000000..76e0b3fe1
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/Products/ProductItem.cs
@@ -0,0 +1,58 @@
+namespace Carts.ShoppingCarts.Products;
+
+public class ProductItem
+{
+ public Guid ProductId { get; }
+
+ public int Quantity { get; }
+
+ private ProductItem(Guid productId, int quantity)
+ {
+ ProductId = productId;
+ Quantity = quantity;
+ }
+
+ public static ProductItem Create(Guid? productId, int? quantity)
+ {
+ if (!productId.HasValue)
+ throw new ArgumentNullException(nameof(productId));
+
+ return quantity switch
+ {
+ null => throw new ArgumentNullException(nameof(quantity)),
+ <= 0 => throw new ArgumentOutOfRangeException(nameof(quantity), "Quantity has to be a positive number"),
+ _ => new ProductItem(productId.Value, quantity.Value)
+ };
+ }
+
+ public ProductItem MergeWith(ProductItem productItem)
+ {
+ if (!MatchesProduct(productItem))
+ throw new ArgumentException("Product does not match.");
+
+ return Create(ProductId, Quantity + productItem.Quantity);
+ }
+
+ public ProductItem Substract(ProductItem productItem)
+ {
+ if (!MatchesProduct(productItem))
+ throw new ArgumentException("Product does not match.");
+
+ return Create(ProductId, Quantity - productItem.Quantity);
+ }
+
+ public bool MatchesProduct(ProductItem productItem)
+ {
+ return ProductId == productItem.ProductId;
+ }
+
+ public bool HasEnough(int quantity)
+ {
+ return Quantity >= quantity;
+ }
+
+ public bool HasTheSameQuantity(ProductItem productItem)
+ {
+ return Quantity == productItem.Quantity;
+ }
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/RemovingProduct/IRemoveProductService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/RemovingProduct/IRemoveProductService.cs
new file mode 100644
index 000000000..43af916f8
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/RemovingProduct/IRemoveProductService.cs
@@ -0,0 +1,6 @@
+namespace Carts.ShoppingCarts.RemovingProduct;
+
+public interface IRemoveProductService
+{
+ void RemoveProduct(Guid cartId, Guid productId, int? quantity, decimal? unitPrice);
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/RemovingProduct/RemoveProductService.cs b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/RemovingProduct/RemoveProductService.cs
new file mode 100644
index 000000000..fe5c955aa
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/ShoppingCarts/RemovingProduct/RemoveProductService.cs
@@ -0,0 +1,22 @@
+using Carts.DataAccess;
+using Carts.ShoppingCarts.Products;
+
+namespace Carts.ShoppingCarts.RemovingProduct;
+
+public class RemoveProductService: IRemoveProductService
+{
+ private readonly IShoppingCartRepository shoppingCartRepository;
+
+ public RemoveProductService(IShoppingCartRepository shoppingCartRepository)
+ {
+ this.shoppingCartRepository = shoppingCartRepository;
+ }
+
+ public void RemoveProduct(Guid cartId, Guid productId, int? quantity, decimal? unitPrice)
+ {
+ var pricedProduct = PricedProductItem.Create(productId, quantity, unitPrice);
+ var cart = this.shoppingCartRepository.GetById(cartId);
+ cart.RemoveProduct(pricedProduct);
+ this.shoppingCartRepository.Save(cart);
+ }
+}
\ No newline at end of file
diff --git a/Sample/ECommerce/CartsMinimalApi/Carts/SumExtensions.cs b/Sample/ECommerce/CartsMinimalApi/Carts/SumExtensions.cs
new file mode 100644
index 000000000..7385bb6fb
--- /dev/null
+++ b/Sample/ECommerce/CartsMinimalApi/Carts/SumExtensions.cs
@@ -0,0 +1,11 @@
+using Carts.ShoppingCarts.Products;
+
+namespace Carts;
+
+public static class SumExtensions
+{
+ public static Money Sum(this IEnumerable source, Func selector)
+ {
+ return source.Select(selector).Aggregate((x, y) => x + y);
+ }
+}
diff --git a/Sample/ECommerce/PracticalEventSourcing.sln b/Sample/ECommerce/PracticalEventSourcing.sln
index 4707393e6..f7c3fdf37 100644
--- a/Sample/ECommerce/PracticalEventSourcing.sln
+++ b/Sample/ECommerce/PracticalEventSourcing.sln
@@ -6,8 +6,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "..\..\Core\Core.csp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Marten", "..\..\Core.Marten\Core.Marten.csproj", "{9268B3EA-71E1-4B50-A459-5732F4977942}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Testing", "..\..\Core.Testing\Core.Testing.csproj", "{8A50B198-C3ED-4DE8-8D63-11FF005C53C1}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.WebApi", "..\..\Core.WebApi\Core.WebApi.csproj", "{72ACB5C8-FD64-4041-BA20-BFCC5A04E97C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Carts", "Carts", "{190C033E-DF29-4F8F-A048-7E860ADD082D}"
@@ -68,6 +66,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Api.Testing", "..\..\C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Serialization", "..\..\Core.Serialization\Core.Serialization.csproj", "{F519CFB6-9B4D-4317-B442-32B5F4EC5A4C}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CartsMinimalApi", "CartsMinimalApi", "{1839223D-398E-451E-B250-06ADEA64AA98}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carts.Api", "CartsMinimalApi\Carts.Api\Carts.Api.csproj", "{E6802F17-997D-4910-ADF6-6CA273D4C827}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Carts", "CartsMinimalApi\Carts\Carts.csproj", "{36387ADB-9DE4-4670-A7EA-1E3D1614D886}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -76,7 +80,6 @@ Global
GlobalSection(NestedProjects) = preSolution
{607CC0DE-712A-4FFB-9862-FEB4F10100D2} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
{9268B3EA-71E1-4B50-A459-5732F4977942} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
- {8A50B198-C3ED-4DE8-8D63-11FF005C53C1} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
{72ACB5C8-FD64-4041-BA20-BFCC5A04E97C} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
{6ED65CEC-8661-483F-A7BE-5B6EAAE7E5A2} = {190C033E-DF29-4F8F-A048-7E860ADD082D}
{AB956DE1-6927-49D3-986C-6298AD92FC04} = {190C033E-DF29-4F8F-A048-7E860ADD082D}
@@ -98,6 +101,8 @@ Global
{714E605E-6EB2-41F4-A2BC-FACD7F781907} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
{3EFCB989-6F16-4D69-8C3D-7F862243FAB9} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
{F519CFB6-9B4D-4317-B442-32B5F4EC5A4C} = {CC9592F6-C639-4C1E-A089-E5A7F4B9BAEA}
+ {E6802F17-997D-4910-ADF6-6CA273D4C827} = {1839223D-398E-451E-B250-06ADEA64AA98}
+ {36387ADB-9DE4-4670-A7EA-1E3D1614D886} = {1839223D-398E-451E-B250-06ADEA64AA98}
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{607CC0DE-712A-4FFB-9862-FEB4F10100D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -108,10 +113,6 @@ Global
{9268B3EA-71E1-4B50-A459-5732F4977942}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9268B3EA-71E1-4B50-A459-5732F4977942}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9268B3EA-71E1-4B50-A459-5732F4977942}.Release|Any CPU.Build.0 = Release|Any CPU
- {8A50B198-C3ED-4DE8-8D63-11FF005C53C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8A50B198-C3ED-4DE8-8D63-11FF005C53C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8A50B198-C3ED-4DE8-8D63-11FF005C53C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8A50B198-C3ED-4DE8-8D63-11FF005C53C1}.Release|Any CPU.Build.0 = Release|Any CPU
{72ACB5C8-FD64-4041-BA20-BFCC5A04E97C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72ACB5C8-FD64-4041-BA20-BFCC5A04E97C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72ACB5C8-FD64-4041-BA20-BFCC5A04E97C}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -196,5 +197,13 @@ Global
{F519CFB6-9B4D-4317-B442-32B5F4EC5A4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F519CFB6-9B4D-4317-B442-32B5F4EC5A4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F519CFB6-9B4D-4317-B442-32B5F4EC5A4C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6802F17-997D-4910-ADF6-6CA273D4C827}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6802F17-997D-4910-ADF6-6CA273D4C827}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6802F17-997D-4910-ADF6-6CA273D4C827}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6802F17-997D-4910-ADF6-6CA273D4C827}.Release|Any CPU.Build.0 = Release|Any CPU
+ {36387ADB-9DE4-4670-A7EA-1E3D1614D886}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {36387ADB-9DE4-4670-A7EA-1E3D1614D886}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {36387ADB-9DE4-4670-A7EA-1E3D1614D886}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {36387ADB-9DE4-4670-A7EA-1E3D1614D886}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal