diff --git a/.dylan/description.md b/.dylan/description.md new file mode 100644 index 0000000..27daf1b --- /dev/null +++ b/.dylan/description.md @@ -0,0 +1,7 @@ +Ten foundational **.NET packages**, versioned in lockstep, each doing one thing and +depending only on the packages below it. + +Clean Architecture with vertical slice structure, made opinionated on purpose: one way +to report a failure, one way to run a handler, one way to turn a failure into a status +code. Where a decision is forced, Loom makes it — where it is taste, it hands you the +object and gets out of the way. diff --git a/.dylan/description.nl.md b/.dylan/description.nl.md new file mode 100644 index 0000000..b388553 --- /dev/null +++ b/.dylan/description.nl.md @@ -0,0 +1,7 @@ +Tien fundamentele **.NET-pakketten**, in lockstep geversioneerd, die elk één ding doen +en alleen afhangen van de pakketten eronder. + +Clean Architecture met vertical slices, bewust uitgesproken: één manier om een fout te +melden, één manier om een handler uit te voeren, één manier om een fout naar een +statuscode te vertalen. Waar een keuze onvermijdelijk is, maakt Loom hem — waar het +smaak is, krijg je het object terug en gaat Loom opzij. diff --git a/.dylan/meta.toml b/.dylan/meta.toml new file mode 100644 index 0000000..1a94fe6 --- /dev/null +++ b/.dylan/meta.toml @@ -0,0 +1,55 @@ +featured = true +weight = 0 + +# Identifiers carry the CodeByDylan. prefix; "Loom." cannot be reserved on nuget.org. +# Loom.Results.Analyzers is omitted deliberately: it ships inside CodeByDylan.Loom.Results. +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Results" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Entities" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Specifications" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Paging" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Handlers.Abstractions" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Handlers" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Handlers.FluentValidation" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Results.AspNetCore" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Persistence.EntityFrameworkCore" + +[[release.packages]] +registry = "nuget" +id = "CodeByDylan.Loom.Templates" + +[[links]] +url = "https://github.com/CodeByDylan/Loom/tree/main/docs/agents" +label = "Guidance for projects built on Loom" +label_nl = "Richtlijnen voor projecten op Loom" + +[[links]] +url = "https://github.com/CodeByDylan/Loom/tree/main/samples" +label = "Sample ordering API" +label_nl = "Voorbeeld-API" + diff --git a/.dylan/star/1-analyzer.md b/.dylan/star/1-analyzer.md new file mode 100644 index 0000000..aa55e2b --- /dev/null +++ b/.dylan/star/1-analyzer.md @@ -0,0 +1,22 @@ +# An analyzer for the one mistake this style makes easy + +## situation + +Returning failures as values instead of exceptions means a discarded `Result` is silently +a swallowed failure. The compiler has nothing to say about it. + +## task + +Catch discarded results at build time, without asking anyone to install or enable +anything. + +## action + +Shipped `LOOM0001` inside `CodeByDylan.Loom.Results`, so it arrives with the package. +Discarding on purpose stays legal by writing `_ =`, which makes the intent visible in +review. + +## result + +Its first run against existing code found eight unchecked discards in this repository's +own test suite. diff --git a/.dylan/star/1-analyzer.nl.md b/.dylan/star/1-analyzer.nl.md new file mode 100644 index 0000000..a3eba1d --- /dev/null +++ b/.dylan/star/1-analyzer.nl.md @@ -0,0 +1,22 @@ +# Een analyzer voor de ene fout die deze stijl makkelijk maakt + +## situation + +Fouten als waarde teruggeven in plaats van als exception betekent dat een weggegooide +`Result` stilzwijgend een genegeerde fout is. De compiler heeft er niets over te zeggen. + +## task + +Weggegooide resultaten opvangen tijdens het bouwen, zonder dat iemand iets hoeft te +installeren of aan te zetten. + +## action + +`LOOM0001` meegeleverd in `CodeByDylan.Loom.Results`, zodat de analyzer met het pakket +meekomt. Bewust weggooien blijft toegestaan door `_ =` te schrijven, waardoor die +bedoeling zichtbaar wordt tijdens review. + +## result + +De eerste run op bestaande code vond acht ongecontroleerde weggooiacties in de testsuite +van deze repository zelf. diff --git a/.dylan/story.md b/.dylan/story.md new file mode 100644 index 0000000..299b8d2 --- /dev/null +++ b/.dylan/story.md @@ -0,0 +1,23 @@ +Every new .NET service started the same way. A `Result` type, rewritten slightly +differently than last time. A folder layout argued about again. A decision about whether +a not-found is an exception or a return value, made again, and made differently. + +None of those are hard problems. That is exactly why re-deciding them is wasteful — the +cost is not the thinking, it is that two services in the same solution end up disagreeing +about what a failure looks like. + +So Loom takes the decisions away. There is one `Error`, with a stable code and one of six +categories, and that category is what decides the status code, the log level, and whether +a retry makes sense. An endpoint becomes a thin adapter with no mapping logic of its own. +Handlers are injected and called directly, so "go to definition" lands on the handler +instead of a registry. + +The harder half was deciding what to leave out. No mediator, because indirection you +cannot navigate is a cost paid on every read. No repository, because `DbContext` is +already a unit of work and wrapping it destroys the `IQueryable` composition that makes +specifications work. No option to log a request's contents — not defaulted off, but +absent, because an option is an invitation. + +Ten packages rather than one, so nothing drags in a framework you did not ask for. They +version in lockstep because a matrix of compatible versions is its own maintenance +problem, and this is meant to remove those, not add one. diff --git a/.dylan/story.nl.md b/.dylan/story.nl.md new file mode 100644 index 0000000..e6609ea --- /dev/null +++ b/.dylan/story.nl.md @@ -0,0 +1,25 @@ +Elke nieuwe .NET-service begon op dezelfde manier. Een `Result`-type, net iets anders +geschreven dan de vorige keer. Een mappenstructuur waar opnieuw over werd gediscussieerd. +De vraag of "niet gevonden" nu een exception is of een retourwaarde, opnieuw beantwoord en +opnieuw anders. + +Geen van die dingen is een moeilijk probleem. Juist daarom is het zonde om ze telkens +opnieuw te beslissen — de kosten zitten niet in het nadenken, maar in het feit dat twee +services binnen dezelfde solution het oneens blijken over hoe een fout eruitziet. + +Loom neemt die beslissingen dus weg. Er is één `Error`, met een vaste code en één van zes +categorieën, en die categorie bepaalt de statuscode, het logniveau en of opnieuw proberen +zinvol is. Een endpoint wordt daarmee een dunne adapter zonder eigen vertaallogica. +Handlers worden geïnjecteerd en direct aangeroepen, zodat "go to definition" bij de handler +uitkomt in plaats van bij een registry. + +De lastigere helft was bepalen wat er níét in moest. Geen mediator, want indirectie die je +niet kunt volgen kost je bij elke keer lezen. Geen repository, want `DbContext` is al een +unit of work, en het inpakken ervan doet de `IQueryable`-compositie teniet waar +specifications juist op leunen. Geen optie om de inhoud van een request te loggen — niet +standaard uitgeschakeld, maar afwezig, want een optie is een uitnodiging. + +Tien pakketten in plaats van één, zodat niets een framework binnenhaalt waar je niet om +hebt gevraagd. Ze worden in lockstep geversioneerd, omdat een matrix van compatibele +versies zelf weer onderhoud oplevert — en dit is bedoeld om dat soort onderhoud weg te +nemen, niet om er iets aan toe te voegen. diff --git a/.github/workflows/portfolio.yml b/.github/workflows/portfolio.yml new file mode 100644 index 0000000..9abb826 --- /dev/null +++ b/.github/workflows/portfolio.yml @@ -0,0 +1,27 @@ +name: Update portfolio + +# Rebuilds dylandebeer.nl, which reads this repo's .dylan directory at build time. +on: + push: + tags: ['v*'] + branches: [main] + paths: ['.dylan/**'] + +concurrency: + group: update-portfolio + cancel-in-progress: true + +jobs: + rebuild: + runs-on: ubuntu-latest + steps: + - name: Trigger a site rebuild + env: + HOOK: ${{ secrets.VERCEL_DEPLOY_HOOK }} + run: | + if [ -z "$HOOK" ]; then + echo "VERCEL_DEPLOY_HOOK is not set; skipping." + exit 0 + fi + curl -fsS -X POST "$HOOK" > /dev/null + echo "Portfolio rebuild triggered."