Add AGENTS.md, dotnet/mise wrapper scripts, migrate yaks to yx

This commit is contained in:
2026-09-10 18:12:50 +01:00
parent 42b3c502a1
commit 7e38f67e0b
7 changed files with 90 additions and 153 deletions
+5
View File
@@ -0,0 +1,5 @@
.yaks
artifacts/
bin/
obj/
*.user
+49
View File
@@ -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 "<yak name>" # full yak context (the task instructions)
yx start "<yak name>" # set state to wip
yx done "<yak name>" # 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 "<name>"` (nest with `--under "<parent>"`).
- 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.
+2
View File
@@ -0,0 +1,2 @@
[tools]
dotnet = "10"
+29
View File
@@ -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 <dotnet command and args>
# 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 "$@"
+5
View File
@@ -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 "$@"
-91
View File
@@ -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)
-62
View File
@@ -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)