Commerce
A fulfilment test may need several orders and shipments, with numbering shared at different levels. This example separates those scopes and keeps checkout inputs unsaved so the code under test can process them.
Customer → Order → LineItem → Shipment → TrackingEvent, plus a transient CheckoutInput that models a request DTO. Each level numbers itself with a different scope. Source: tests/Zeeq.Biome.Tests/Commerce.
One seed, four scopes
| Sequence | Scope | Reads as |
|---|---|---|
OrderNumbers | .Per<Customer>() | orders count up across a customer's whole history |
LineNumbers | .PerParent() | each order starts its own line numbering |
ShipmentNumbers | .Per<Customer>() | shipment numbers span all lines and orders for the customer |
EventNumbers | .PerParent() | a shipment's checkpoints are ordered independently |
// Shipments have line parents, but numbering spans
// all lines/orders for the customer.
internal static readonly Sequence<int> ShipmentNumbers = Sequence
.Of(1, n => n + 1)
.Per<Customer>();
// Leave Number/Reference unset: call-site overrides
// run before these fill-only derives.
internal static readonly ChildBlueprint<CommerceSeed, Customer, Order> Order =
Blueprint
.ChildOf<CommerceSeed, Customer, Order>(
(customer, ctx) =>
new()
{
Customer = customer,
PlacedAt = ctx.Ago(TimeSpan.FromDays(2)),
}
)
.Derive(o => o.Number, OrderNumbers)
.Derive(o => o.Reference, (o, _) => $"ORDER-{o.Number}");
// Reach through Line -> Order -> Customer; relationships never depend on Last<T>().
internal static readonly ChildBlueprint<
CommerceSeed,
LineItem,
Shipment
> Shipment = Blueprint
.ChildOf<CommerceSeed, LineItem, Shipment>(
(line, ctx) =>
new()
{
Line = line,
Customer = ctx.Ancestor<Customer>(),
ExpectedAt = ctx.FromNow(TimeSpan.FromDays(1)),
}
)
.Derive(s => s.CustomerNumber, ShipmentNumbers)
.Derive(s => s.Label, (s, _) => $"PARCEL-{s.CustomerNumber}");
A busy store in one chain
Counts are per parent, so each line multiplies the previous output: 2 customers × 3 orders × 4 lines × 1 shipment × 3 events.
var biome = TestBiome.Start(out _, store);
await biome
.AddCustomers(out var customers, 2)
.AddOrders(out var orders, 3) // 6 orders
.AddLineItems(out var lines, 4) // 24 lines
.AddShipments(out var shipments, 1) // 24 shipments
.AddTrackingEvents(out var events, 3) // 72 checkpoints
.Under(orders) // sibling branch: checkout inputs hang off orders
.AddCheckoutInputs(out var inputs, 1) // AsTransient lives on the blueprint
.BuildAsync();
var customers = Enumerable
.Range(0, 2)
.Select(_ => new Customer { Name = "Example shop shopper" })
.ToArray();
var now = DateTimeOffset.UtcNow;
var orders = new List<Order>();
var lines = new List<LineItem>();
var shipments = new List<Shipment>();
var events = new List<TrackingEvent>();
foreach (var customer in customers)
{
var orderNumber = 1;
var shipmentNumber = 1; // per customer, across orders
for (var o = 0; o < 3; o++)
{
var order = new Order
{
Customer = customer,
Number = orderNumber,
Reference = $"ORDER-{orderNumber}",
PlacedAt = now.AddDays(-2),
};
orderNumber++;
orders.Add(order);
for (var l = 0; l < 4; l++)
{
var line = new LineItem
{
Order = order,
Number = l + 1,
Sku = $"SKU-{l + 1}",
};
lines.Add(line);
var shipment = new Shipment
{
Line = line,
Customer = customer,
CustomerNumber = shipmentNumber,
Label = $"PARCEL-{shipmentNumber}",
ExpectedAt = now.AddDays(1),
};
shipmentNumber++;
shipments.Add(shipment);
for (var e = 0; e < 3; e++)
{
events.Add(
new TrackingEvent
{
Shipment = shipment,
Customer = customer,
Number = e + 1,
Description = $"Checkpoint {e + 1}",
OccurredAt =
now.AddHours(-24) + TimeSpan.FromMinutes(e + 1),
}
);
}
}
}
}
db.AddRange(customers);
db.AddRange(orders);
db.AddRange(lines);
db.AddRange(shipments);
db.AddRange(events);
await db.SaveChangesAsync();
Assertions the real test makes: biome.All<Order>().Count == 6, lines[4].Number == 1 (a new order restarts), shipments[4].CustomerNumber == 5 (spans orders), shipments[12].CustomerNumber == 1 (a new customer restarts), and the store holds 2 + 6 + 24 + 24 + 72 rows, no CheckoutInput.
Transient request inputs
CheckoutInput is built next to the stored entities but never saved. AsTransient() lives on the blueprint, so every use is excluded:
// Request DTOs can be built alongside stored
// entities; transient is a blueprint variant.
internal static readonly ChildBlueprint<
CommerceSeed,
Order,
CheckoutInput
> Checkout = Blueprint
.ChildOf<CommerceSeed, Order, CheckoutInput>(
(order, _) => new() { Order = order }
)
.Derive(i => i.Reference, (i, _) => i.Order.Reference)
.AsTransient();
Traits, explicit numbers, resuming a customer
+ makes one order cancelled and priority; the comma makes another. The explicit Number = 1 is skipped by the generator, so the first order becomes 2, and .Under(customer) resumes the customer's sequence.
await biome
.AddCustomer(out var customer)
.AddOrders(
out var orders,
Orders.Cancelled + Orders.Priority,
// Call site wins: 1 is explicit, so the first order skips to 2.
o => o.Number = 1
)
.AddLineItems(out var lines, 2)
.AddShipments(out var shipments, 1)
.AddTrackingEvents(out _, 2)
// Resume the customer's order sequence rather than starting from a leaf anchor.
.Under(customer)
.AddOrder(out var next)
.BuildAsync();
// orders[0].Number == 2 ("ORDER-2"), orders[1].Number == 1, next.Number == 3
// lines[2].Number == 1 (local reset)
// shipments[2].CustomerNumber == 3 (customer-wide)
A helper for the deep default branch
public static class CommerceScenarioHelpers
{
extension(BiomeBuilder<CommerceSeed, CommerceSeed> b)
{
public BiomeBuilder<CommerceSeed, TrackingEvent[]> AddTrackedOrder(
out TrackingEvent leaf
) =>
b.AddCustomer(out _)
.AddOrder(out _)
.AddLineItem(out _)
.AddShipment(out _)
.AddTrackingEvent(out leaf);
}
}
The helper pairs with a shared clock to add a later checkpoint, as shown in Time and precision.