From 42b3c502a14fe0de4683c66a4dd5719438b499dd Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Thu, 10 Sep 2026 18:04:46 +0100 Subject: [PATCH] Add yaks 01 (before: fake EF DbBase inheritance) and 02 (after: DTOs + mappers) for the next agent --- yak/yak-01-before.md | 91 ++++++++++++++++++++++++++++++++++++++++++++ yak/yak-02-after.md | 62 ++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 yak/yak-01-before.md create mode 100644 yak/yak-02-after.md diff --git a/yak/yak-01-before.md b/yak/yak-01-before.md new file mode 100644 index 0000000..9b06918 --- /dev/null +++ b/yak/yak-01-before.md @@ -0,0 +1,91 @@ +# Yak 01 — C# solution: the BEFORE situation (DB-backed domain classes) + +## Context + +This exercise illustrates the transition from domain classes that inherit a DB +base class (EF-style, or active-record-like) to plain domain objects + DTOs. +See `before-after-dto.mmd` (and `before-after-dto-sketch.png`) for the diagram +this yak implements in C#: + +- `DbContext` — the database context (EF fake) +- `DbBase` — base class with an `Id`; depends on `DbContext` +- `Client`, `Order` — domain classes **inheriting `DbBase`** +- `WebApp` — consumes the domain classes directly + +Yak 02 will implement the "after" situation in the same solution. + +## Tech constraints + +- .NET SDK: target **net8.0** (adjust if only another SDK is installed; note it + in the commit message). +- **Fake the EF magic — do NOT add real EF Core.** The point of the exercise is + the *shape of the dependencies*, not EF's behavior. A fake `DbContext` keeps + the project pure managed code: no native binaries, no SQLite, no provider + quirks → guaranteed cross-platform (Windows/macOS/Linux). +- Tests: **xUnit**, one test project. +- Solution layout: + + ``` + db-subclass-to-dto.sln + src/ + Before/ (net8.0 class library, namespace Before.*) + After/ (net8.0 class library, namespace After.*) ← created in Yak 02 + tests/ + BeforeAfter.Tests/ (net8.0 xUnit project) + ``` + + Yak 01 only creates the solution, the `Before/` project, and the test project + (plus an empty `After/` project so the solution shape is final). + +## "Magic" to fake on DbBase / DbContext + +Simulate EF's change tracker with a minimal fake: + +- `DbContext` holds a registration dictionary of entities it knows about. +- `DbContext.Save()` iterates tracked `DbBase` entities: assigns a fresh `Guid` + `Id` to any entity whose `Id` is `Guid.Empty`, and records it. +- `DbBase` carries a back-reference to its owning `DbContext` + (active-record-style "I know which context created me") — this is the + dependency leak the exercise wants to expose. +- `Client` has an `Orders` collection; `Order` has a `Client` back-reference. + Adding an order to `client.Orders` sets `order.Client` (mimic EF navigation + fix-up). +- A `WebApp` type with methods that consume `Client`/`Order` directly, + including one that demonstrates the leak: reaching the `DbContext` *through* + a domain object (e.g. `client.DbContext`). + +Keep the fake small; this is an illustration, not an ORM. + +## Required tests (BeforeTests) + +At minimum: + +1. `Client` and `Order` derive from `DbBase` + (`typeof(DbBase).IsAssignableFrom(typeof(Client))` etc.). +2. New `Client`/`Order` instances have `Id == Guid.Empty`. +3. `DbContext.Save()` assigns fresh, distinct `Guid` ids to saved entities + (the faked EF magic). +4. Saved entities are registered with their `DbContext` (reachable via the + back-reference). +5. Relation fix-up: `client.Orders.Add(order)` sets `order.Client == client`. +6. The leak: from a bare `Client`, you can reach the `DbContext` + (`client.DbContext` is non-null after save) — assert this explicitly, + comment that this is exactly what the "after" situation removes. +7. `WebApp` methods work on the domain classes directly. + +## Acceptance criteria + +- `dotnet build db-subclass-to-dto.sln` succeeds with 0 warnings. +- `dotnet test db-subclass-to-dto.sln` succeeds; all tests pass. +- No NuGet packages beyond xUnit's default set (no EF Core, no SQLite, + no AutoMapper). +- Code + tests committed with a message referencing this yak. + +## Open questions (answer before starting, defaults in parens) + +1. Target framework net8.0 or something else? (net8.0) +2. Fake DbContext only, or do you also want a variant using real EF Core + + `Microsoft.Data.Sqlite` (bundled native lib — still no install needed on + Windows, but adds NuGet/ABI surface)? (fake only) +3. Any naming/style preference (file-scoped namespaces, records vs classes)? + (file-scoped namespaces; plain classes) \ No newline at end of file diff --git a/yak/yak-02-after.md b/yak/yak-02-after.md new file mode 100644 index 0000000..78641ba --- /dev/null +++ b/yak/yak-02-after.md @@ -0,0 +1,62 @@ +# Yak 02 — C# solution: the AFTER situation (plain domain + DTOs + mappers) + +## Context + +Depends on Yak 01 (solution, `Before/` project, test project already exist). +Implement the "after" half of the exercise in the existing `After/` project +and add tests. See `after-dto.mmd` for the diagram: + +- `Client`, `Order` — **plain** domain objects, no `DbBase` inheritance, + no knowledge of `DbContext` +- `ClientDto`, `OrderDto` — DTOs carrying the same data **including the + relations** (`ClientDto.Orders`, back-references) +- `Mappers` — hand-written mapping, each way: + `ToClientDto`, `FromClientDto`, `ToOrderDto`, `FromOrderDto` +- `DbContext`/`DbBase` — still exist (persistence layer), but the domain no + longer subclasses/leaks them +- `WebApp` — consumes **only DTOs** + +## Rules + +- Do NOT modify `Before/` (it's the frozen "before" state). +- Hand-write the mappers (no AutoMapper) — the mapping code is part of the + illustration. Mappers must not mutate their inputs. +- Keep the fake `DbContext`/`DbBase` for persistence in `After/` (or reuse the + idea from `Before/`), but the domain types must not derive from `DbBase`. + Persisting a domain object now requires explicit registration (e.g. a + `DbContext.Save(client)` that copies into a tracked entity, or registers an + adapter) — the direction of dependency is flipped. + +## Required tests (AfterTests) + +At minimum, mirroring the Before tests where meaningful: + +1. `Client`/`Order` do **not** derive from `DbBase`, and (via reflection) + expose no `DbContext`-typed property or field — the leak is gone. +2. Mappers map all scalar fields, in both directions + (`Client → ClientDto`, `ClientDto → Client`, same for `Order`). +3. Mappers map the relations: `Client.Orders` → `ClientDto.Orders`, and the + `Order.Client` / `OrderDto.ClientDto` back-references stay consistent. +4. Round-trip: `FromX(ToX(domain))` equals the original domain state + (compare by values, not reference); same for DTO round-trip. +5. Mappers do not mutate their input (snapshot fields, map, compare). +6. `WebApp` (after version) methods consume only `ClientDto`/`OrderDto`; + assert the `After.WebApp` type references no domain or DbContext type in + its public API (reflection over member signatures). +7. Persistence still works through the fake `DbContext` in the after world. + +## Acceptance criteria + +- `dotnet build` and `dotnet test` succeed on the full solution + (Before + After + tests), 0 warnings. +- No NuGet packages beyond xUnit's default set. +- `Before/` project unmodified (diff check). +- Committed with a message referencing this yak. + +## Open questions (answer before starting, defaults in parens) + +1. How should the after-world persistence flip look — explicit + `ctx.Save(domainObject)` with internal entity/DTO translation, or something + else? (explicit Save with translation, minimal) +2. Are DTOs records (value equality helps round-trip tests) or plain classes? + (records) \ No newline at end of file