diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b81c7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.yaks +artifacts/ +bin/ +obj/ +*.user \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c79d4ca --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,49 @@ +# AGENTS.md + +Exercise repo: illustrating the transition from DB-backed domain classes +(`DbBase` inheritance) to plain domain objects + DTOs + mappers. +Diagrams: `before-after-dto.mmd`, `after-dto.mmd` (source sketch: +`before-after-dto-sketch.png`). + +## Running dotnet + +dotnet SDK 10 is installed **via mise** (see `mise.toml`). It is NOT on PATH in +agent/sandbox shells, and SDK 10 additionally fails without +`DOTNET_CLI_HOME` in some shells. Always use the wrappers: + +```sh +scripts/dotnet.sh build db-subclass-to-dto.sln +scripts/dotnet.sh test db-subclass-to-dto.sln +scripts/run-tests.sh # full test suite, from repo root +``` + +The wrappers cd to the repo root (the test runner must run from the parent +directory of the test project), export `DOTNET_CLI_HOME`, and invoke +`mise exec -- dotnet ...`. Do not call bare `dotnet`. + +## Yaks (task management with yx) + +Tasks are managed with the `yx` CLI; yaks live in git (event store), snapshot +in `.yaks/` (gitignored). Work one yak at a time: + +```sh +yx list # all yaks and states +yx show "" # full yak context (the task instructions) +yx start "" # set state to wip +yx done "" # set state to done (only after acceptance + # criteria pass and changes are committed) +``` + +- The yak context in `yx show` is the authoritative task description; follow + its acceptance criteria before `yx done`. +- Children block their parent: do prerequisites (nested under a yak) first. +- Add new tasks: `yx add ""` (nest with `--under ""`). +- If yx complains `.yaks is not gitignored`, keep `.yaks` in `.gitignore`. + +## Conventions + +- Target framework: net10.0 (SDK pinned by mise). +- Tests: xUnit, one `tests/BeforeAfter.Tests/` project. +- No EF Core / SQLite / AutoMapper — the DB layer is a small hand-rolled fake + (pure managed code, cross-platform). +- Commit per yak with a message referencing the yak. \ No newline at end of file diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..df386ef --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +dotnet = "10" diff --git a/scripts/dotnet.sh b/scripts/dotnet.sh new file mode 100755 index 0000000..3e5b137 --- /dev/null +++ b/scripts/dotnet.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Run any dotnet command for this repo. +# +# Why a wrapper: +# - dotnet is installed via mise (see mise.toml, dotnet = "10"); it is NOT on +# PATH in agent/sandbox shells, so plain `dotnet` fails with 127. +# - dotnet SDK 10 fails with "The user's home directory could not be +# determined" unless DOTNET_CLI_HOME is set. +# - The test runner must be invoked from the repo root (the parent directory +# of the test project), so we always cd here first. +# +# Usage: scripts/dotnet.sh +# scripts/dotnet.sh test db-subclass-to-dto.sln +# scripts/dotnet.sh build +set -euo pipefail + +cd "$(dirname "$0")/.." +export DOTNET_CLI_HOME="${DOTNET_CLI_HOME:-$HOME/.dotnet-cli}" +export DOTNET_NOLOGO=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + +if ! command -v mise >/dev/null 2>&1; then + echo "error: mise not found on PATH; install mise or put dotnet on PATH" >&2 + exit 1 +fi +if [ "$#" -eq 0 ]; then + exec mise exec -- dotnet --version +fi +exec mise exec -- dotnet "$@" \ No newline at end of file diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh new file mode 100755 index 0000000..7716443 --- /dev/null +++ b/scripts/run-tests.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Run the full test suite from the repo root (see scripts/dotnet.sh for why). +# Usage: scripts/run-tests.sh [extra dotnet test args] +set -euo pipefail +exec "$(cd "$(dirname "$0")" && pwd)/dotnet.sh" test db-subclass-to-dto.sln "$@" \ No newline at end of file diff --git a/yak/yak-01-before.md b/yak/yak-01-before.md deleted file mode 100644 index 9b06918..0000000 --- a/yak/yak-01-before.md +++ /dev/null @@ -1,91 +0,0 @@ -# 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 deleted file mode 100644 index 78641ba..0000000 --- a/yak/yak-02-after.md +++ /dev/null @@ -1,62 +0,0 @@ -# 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