From 53586d33a42f7cf5c6e397a7f62e64ce4b5f17f4 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 16 Sep 2026 22:17:28 +0100 Subject: [PATCH] identity read-back: FromOrderData/FromClientData + DbContext.LoadOrder/LoadClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plain field-copy materialization so the persistence round-trip closes. When OrderLines arrive, FromOrderData becomes the one-line reconstruction (row description + total become a single line) — the hinge Exercise 2 pivots on. yak: orderdata-read-back-identity-version-j9ew [order-data-mapping] --- src/After/DbContext.cs | 40 +++++++++++++ src/After/Mappers.cs | 25 ++++++++ .../AfterPersistenceTests.cs | 57 +++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/src/After/DbContext.cs b/src/After/DbContext.cs index 3651c04..ff0e53b 100644 --- a/src/After/DbContext.cs +++ b/src/After/DbContext.cs @@ -115,6 +115,46 @@ public class DbContext _byRef[order] = fresh; } + // ── loading: reassemble domain objects from the stored rows ────── + + /// + /// Materialize the stored under , + /// including all of its orders (navigation fix-up wires + /// back). Null when nothing is stored under it. + /// + public Client? LoadClient(Guid id) + { + var row = Find(r => r.Id == id); + if (row is null) return null; + var client = Mappers.FromClientData(row); + foreach (var orderRow in _orders.Where(o => o.ClientId == id)) + client.Orders.Add(Mappers.FromOrderData(orderRow)); // fix-up wires Order.Client + return client; + } + + /// + /// Materialize the stored under , + /// including its client (holding just this one order), or null when + /// nothing is stored under it. Use to load a + /// client with all of its orders. + /// + public Order? LoadOrder(Guid id) + { + var row = Find(r => r.Id == id); + if (row is null) return null; + var order = Mappers.FromOrderData(row); + if (row.ClientId != Guid.Empty) + { + var clientRow = Find(c => c.Id == row.ClientId); + if (clientRow is not null) + { + var client = Mappers.FromClientData(clientRow); + client.Orders.Add(order); // fix-up wires Order.Client + } + } + return order; + } + // ── query helpers ─────────────────────────────────────────────── /// Search the per-type table for a record whose predicate matches. diff --git a/src/After/Mappers.cs b/src/After/Mappers.cs index 520ec01..c9f016b 100644 --- a/src/After/Mappers.cs +++ b/src/After/Mappers.cs @@ -138,4 +138,29 @@ public static class Mappers ClientId = order.Client?.Id ?? Guid.Empty, }; } + + // ------------------------------------------------------------------ + // Persistence -> domain (read-back). A plain field copy for now; when + // the domain grows OrderLines, FromOrderData becomes the one-line + // reconstruction (row description + total become a single line) — + // see EXERCISE2.md. + // ------------------------------------------------------------------ + + /// Map a record back to a . + public static Client FromClientData(ClientData row) + { + ArgumentNullException.ThrowIfNull(row); + return new Client { Id = row.Id, Name = row.Name }; + } + + /// + /// Map an record back to an . + /// Reassembling the graph (wiring ) is the + /// context's job, not the mapper's. + /// + public static Order FromOrderData(OrderData row) + { + ArgumentNullException.ThrowIfNull(row); + return new Order { Id = row.Id, Description = row.Description, TotalInCents = row.TotalInCents }; + } } \ No newline at end of file diff --git a/tests/BeforeAfter.Tests/AfterPersistenceTests.cs b/tests/BeforeAfter.Tests/AfterPersistenceTests.cs index 08d1478..eb859a3 100644 --- a/tests/BeforeAfter.Tests/AfterPersistenceTests.cs +++ b/tests/BeforeAfter.Tests/AfterPersistenceTests.cs @@ -88,4 +88,61 @@ public class AfterPersistenceTests Assert.Equal(order.Id, found!.Id); Assert.Null(db.Find(row => row.Description == "no such order")); } + + // --------------------------------------------------------------------- + // Read-back: LoadOrder / LoadClient reassemble domain objects from the + // stored rows (identity round-trip for now — OrderLines come later). + // --------------------------------------------------------------------- + + [Fact] + public void Loading_an_order_restores_its_state_and_its_client() + { + var db = new DbContext(); + var client = new Client { Name = "Acme" }; + var order = new Order { Description = "one", TotalInCents = 150_00 }; + client.Orders.Add(order); + db.Save(client); + db.Save(order); + + var loaded = db.LoadOrder(order.Id); + + Assert.NotNull(loaded); + Assert.Equal(order.Id, loaded!.Id); + Assert.Equal("one", loaded.Description); + Assert.Equal(150_00, loaded.TotalInCents); + + // The graph is reassembled: the loaded order knows its client again, + // and the client's collection holds the loaded order. + Assert.NotNull(loaded.Client); + Assert.Equal("Acme", loaded.Client!.Name); + Assert.Same(loaded, Assert.Single(loaded.Client.Orders)); + } + + [Fact] + public void Loading_a_client_restores_it_with_all_of_its_orders() + { + var db = new DbContext(); + var client = new Client { Name = "Acme" }; + client.Orders.Add(new Order { Description = "one", TotalInCents = 150_00 }); + client.Orders.Add(new Order { Description = "two", TotalInCents = 250_00 }); + db.Save(client); + db.Save(client.Orders[0]); + db.Save(client.Orders[1]); + + var loaded = db.LoadClient(client.Id); + + Assert.NotNull(loaded); + Assert.Equal("Acme", loaded!.Name); + Assert.Equal(2, loaded.Orders.Count); + Assert.All(loaded.Orders, o => Assert.Same(loaded, o.Client)); + } + + [Fact] + public void Loading_an_unknown_id_returns_null() + { + var db = new DbContext(); + + Assert.Null(db.LoadOrder(Guid.NewGuid())); + Assert.Null(db.LoadClient(Guid.NewGuid())); + } }