6.5 KiB
Exercise: the discount that wouldn't map
You are in the "after" situation of this code base: the domain objects
(Client, Order) are plain — no DbBase inheritance, no knowledge of the
DbContext. Two hand-written mapping families lead out of the domain:
OrderDto/ClientDto— transport shapes consumed by the WebApp;OrderData/ClientData— the data-only records the persistence framework stores (one row per object, object graph flattened to ids).
The before situation was Active Record: the domain object and the stored shape were one and the same class, so there was nothing to map. Now mapping is explicit — and that is where this exercise lives.
What you get
Order.TotalInCents— the order total, an integer in cents, persisted and mapped one-on-one intoOrderDtoandOrderData.DiscountCalculator.CalculateDiscount(order, discountBarrierCents, discountPercentage)— a pure domain service. It returns aDiscountedOrder(a separate domain type, correlated to the order byOrderId) carrying the order's total, the percentage actually applied (0 when the total does not strictly exceed the barrier), and the discounted total. Rounding is half-up, in integer math.- Unit tests for all of the above already pass.
The challenge
The DiscountedOrder is not a one-on-one copy of the order, and it must
not become one:
Orderneeds to learn the discounted total — but neitherOrderDtonorOrderDatamay gain it. The discount is derived data: it lives in the domain only, and the storage schema stays untouched.- The
DiscountedOrder.DiscountPercentageis not copied onto the order — it can be derived from the two totals (andTotalInCentsnever changes).
So the flow is: order → DiscountedOrder (calculate) → order (correlate +
copy, dropping what is derivable) → persistence (which stores less than the
domain now knows).
Work through the three steps below — each is small enough to hand to a coding agent as a single instruction. Run the tests after each step:
scripts/run-tests.sh
Step 1 — teach the order its discounted total
Add a single property DiscountedTotalInCents (int, default 0) to the
domain Order in src/After/Order.cs. Do not touch OrderDto,
OrderData, or the mappers: the discounted total must exist in the domain
only. Acceptance: the solution builds and the full test suite still passes.
Step 2 — correlate a discounted order onto its order
Create tests/BeforeAfter.Tests/DiscountedOrderMappingTests.cs from the stub
below and make the first test pass. The point is the correlation: the
DiscountedOrder knows the id of the order it was calculated from — use it
instead of assuming there is only one order around.
using After;
namespace BeforeAfter.Tests;
/// <summary>
/// EXERCISE — the mapping from a DiscountedOrder onto an Order is NOT
/// one-on-one. Make these tests pass, following EXERCISE.md step by step.
/// </summary>
public class DiscountedOrderMappingTests
{
[Fact] // EXERCISE STEP 2
public void A_discounted_order_can_be_correlated_onto_its_order()
{
var db = new DbContext();
var client = new Client { Name = "Acme" };
var order = new Order { Description = "big order", TotalInCents = 150_00 };
client.Orders.Add(order);
db.Save(client);
db.Save(order);
// 10 % for orders above €100:
var discounted = DiscountCalculator.CalculateDiscount(
order, discountBarrierCents: 100_00, discountPercentage: 10);
// TODO (step 2): correlate the discounted order to the saved order and
// copy the discounted total onto the order.
// Hints: DiscountedOrder.OrderId identifies the order; the discount
// percentage is deliberately NOT copied — it is derivable.
// ... your code here ...
Assert.Equal(135_00, order.DiscountedTotalInCents);
}
[Fact] // EXERCISE STEP 3
public void Saving_a_discounted_order_keeps_the_stored_total_untouched()
{
var db = new DbContext();
var client = new Client { Name = "Acme" };
var order = new Order { Description = "big order", TotalInCents = 150_00 };
client.Orders.Add(order);
db.Save(client);
db.Save(order);
var discounted = DiscountCalculator.CalculateDiscount(
order, discountBarrierCents: 100_00, discountPercentage: 10);
// TODO (step 3): correlate + copy as in step 2, then save the updated
// order through the context. Afterwards find the order's persisted
// OrderData row (hint: db.Find<OrderData>(...)) and assert that the
// stored total is STILL the original 150_00: the database knows
// nothing about discounts — derived data lives in the domain only.
// ... your code here ...
OrderData row = null!; // TODO (step 3): find the OrderData row for this order
Assert.Equal(150_00, row.TotalInCents);
}
}
Acceptance: the first test is green, the second still red.
Step 3 — save, and check what actually persisted
Make the second test pass: re-save the updated order through the context
(the context refreshes the stored row in place), then assert on the persisted
OrderData row. Note what you are not asserting: there is no discounted
column to check — the row simply keeps the original total.
Acceptance: the whole suite is green — including the pre-existing tests, which guard exactly the invariants you must not break (the DTO and the persistence records still map the same fields as before).
Facilitator notes
- Expected end state per step: (1) one new domain-only property; (2) a mapping
fragment outside the
Mappersclass — correlation by id plus a field copy, dropping the derivable percentage; (3)db.Save(order)after which theOrderDatarow still showsTotalInCents == 150_00. - Discussion points: why two mapping families out of the domain (transport vs
persistence) and why neither carries the discount; what an Active Record
"before" world would have done here (the discounted value would have been
a column, or nothing at all); what happens to
DiscountedTotalInCentswhen the order travels to the WebApp and back (the DTO round-trip silently drops it — derived data must be recomputed, not transported). - The unit tests in
DiscountCalculatorTests.cspin the calculation contract (strict barrier, half-up rounding, validation); participants should not need to touch them.