In this exercise we will enforce the invariant that an order cannot be too heavy. Since there is not a "Shipping" or "Fulfillment" service, the "Order" service will have to suffice.
In order to enforce our invariant that heavy orders should be rejected, we first need to record the weight for items added.
First, we'll add the weight value to the OrderItem
class, which requires the value in the constructor:
/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs
:
public OrderItem(int productId, string productName, decimal unitPrice, decimal discount, string PictureUrl,
decimal weight, int units = 1)
Create a private field for the weight:
private readonly decimal _weight;
Set this value in the OrderItem
constructor:
_weight = weight;
Also add a method to expose this value:
public decimal GetWeight() => _weight;
EF Core does not yet understand it needs to save the weight on the order item. Modify the order item configuration to include this value:
src\Services\Ordering\Ordering.Infrastructure\EntityConfigurations\OrderItemEntityTypeConfiguration.cs
orderItemConfiguration
.Property<decimal>("_weight")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("Weight")
.IsRequired()
.HasDefaultValue(0m);
Next, we'll need to add a migration to include this new column in the database. If you haven't already, add .NET CLI EF tools from a command prompt:
dotnet tool install --global dotnet-ef
The design tools to enable migrations aren't installed yet, so add a package to the Ordering.Infrastructure.csproj
project:
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0" />
Finally, in the command prompt, navigate to the folder for the project that contains the Order migrations:
src\Services\Ordering\Ordering.API
and use the EF Core .NET CLI tools to add a new migration:
dotnet ef migrations add AddOrderItemWeight --context OrderingContext
This will add a migration and apply it to our database. If this fails because the database is not running, use Docker Desktop to start your container with an image named mcr.microsoft.com/mssql/server:2019-latest
.
Now that our OrderItem
entity requires a weight, we'll need to include this value in our aggregate root's methods.
First, modify the AddOrderItem
method to include a weight:
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs
:
public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl,
decimal weight,
int units = 1)
Next, pass this value through to the OrderItem
constructor later in the method:
var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, weight, units);
Orders are created through command objects, so we'll need to modify our command and handler to pass this value through to our aggregate.
First, we'll modify the command, in the OrderItemDTO
class:
src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommand.cs
:
public decimal Weight { get; init; }
Next, the handler:
src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderCommandHandler.cs
:
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Weight, item.Units);
We'll need to modify the Draft order handler as well:
src/Services/Ordering/Ordering.API/Application/Commands/CreateOrderDraftCommandHandler.cs
:
order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Weight, item.Units);
And finally building up the OrderDraftDTO at the end of the file in the OrderDraftDTO.FromOrder
method:
ProductName = oi.GetOrderItemProductName(),
Weight = oi.GetWeight()
Now that we have our command handler ready to accept the weight, we need to pass this value from our basket.
First, we need to modify the BasketItem
contract in the Ordering
service:
src/Services/Ordering/Ordering.API/Application/Models/BasketItem.cs
public decimal Weight { get; init; }
Next, pass this value through in our method that build up this command DTO in the ToOrderItemDTO
method:
/src/Services/Ordering/Ordering.API/Extensions/BasketItemExtensions.cs
:
Units = item.Quantity,
Weight = item.Weight
The unit tests use a builder pattern that hasn't been fixed for the new data, so let's do that. First, the builder:
src/Services/Ordering/Ordering.UnitTests/Builders.cs
:
public OrderBuilder AddOne(int productId,
string productName,
decimal unitPrice,
decimal discount,
string pictureUrl,
decimal weight,
int units = 1)
{
order.AddOrderItem(productId, productName, unitPrice, discount, pictureUrl, weight, units);
return this;
}
And finally our unit test:
src/Services/Ordering/Ordering.UnitTests/Domain/OrderAggregateTest.cs
:
when_add_two_times_on_the_same_item_then_the_total_of_order_should_be_the_sum_of_the_two_items
test:
var order = new OrderBuilder(address)
.AddOne(1, "cup", 10.0m, 0, string.Empty, 5m)
.AddOne(1, "cup", 10.0m, 0, string.Empty, 5m)
.Build();
Other tests should be updated too, but we'll leave those out for now to get things compiling.
Now that our order has a weight, we can enforce a weight restriction when updating the shipping status (since there isn't an actual shipping service).
First, we'll need to calculate the weight of an order item:
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/OrderItem.cs
:
public decimal GetTotalWeight()
{
return _weight * _units;
}
Next, we'll need to calculate the total order weight:
src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs
:
public decimal GetTotalWeight()
{
return OrderItems.Sum(oi => oi.GetTotalWeight());
}
Finally, when we calculate shipping status, we can check the weight in the SetShippedStatus
method:
public void SetShippedStatus()
{
if (GetTotalWeight() > 1000)
{
throw new OrderingDomainException("Too big.");
}
Now we can run the application, try to add too many items with too much weight, and our order should not ship.