Offer the repository layout, with tooling to create and maintain it - #2
Merged
Merged
Conversation
The tools documented the Apps/ and Libs/ layout but there was no way to create one — AddEntity assumed projects that nothing could scaffold. This adds the missing half, so the structure is something you can adopt rather than something you have to reproduce by hand. It stays optional. Nothing in the packages reads magiccsharp.json or cares where a file sits; a consumer using the packages in their own structure is unaffected. New tools - InitRepo sets a repository up: magiccsharp.json, Directory.Build.props, central package management, the all-projects solution, Apps/ and Libs/. Skips whatever exists, so it composes with a repository already running. - CreateApp creates a service: host project, its own solution, and the pair of data projects that keep repository contracts separate from their EF implementation. --no-database for a service owning no tables. Picks a port no other service has claimed. - CreateAppLib creates a domain: Default for use cases, Models for entities, Tests. Wires Default to Models and never the reverse, since Models is what the data projects depend on. - CreateLib creates a shared library under Libs/, dots nesting directories. Documentation docs/repository-layout.md covers setup, what each boundary is for, what belongs in Libs/ and what does not, and the tool reference. tools/README.md becomes a short reference pointing at it. The root README gains the layout as an explicitly optional section. Two things the end-to-end test caught - The generated Directory.Build.props applied to the tools themselves, so a repo targeting net9.0 produced scripts the net10 runtime refused to launch. tools/Directory.Build.props now pins the scripts and does not inherit the repository's settings. - Generated repos targeted net9.0. The libraries stay there so net9 apps can consume them, but a newly scaffolded repo should not start a version behind; templates now generate net10.0. publish-all.sh keeps InitRepo's pinned version in step with the release, so scaffolding cannot point at a version predating its own tooling. Verified from an empty directory: InitRepo, CreateApp, CreateAppLib, CreateLib and AddEntity produce a repository that builds against the packed packages, passes ValidateConventions, and serves a request with the request-ID middleware live. Every tool is idempotent — second runs produce no diff.
tools/README.md had been slimmed to a table when the layout guide was added, which left it pointing at the guide for everything and carrying no usage of its own. SyncAllProjects had no worked example anywhere. Each tool now gets a section with its real options and runnable commands, verified against the CommandOption declarations rather than written from memory. SyncAllProjects gains the two occasions you actually run it by hand: a conflicted solution file after a merge, and moving a project outside the tools. The guide keeps the narrative and now points at the reference for options.
Adopting the layout meant cloning this repository and copying tools/ out of
it — for a consumer who otherwise only needs the NuGet packages, with no
version link between the scripts they copied and the packages they
reference, and no way to update.
dotnet new install MagicCSharp.Templates
dotnet new magiccsharp-repo -n Acme
Lays down magiccsharp.json, Directory.Build.props, central package
management, the all-projects solution, Apps/, Libs/ and tools/, with the
prefix substituted everywhere — including the scripts' own --help examples,
so a generated repository's tooling talks about its own namespaces.
--MagicCSharpVersion and --TargetFramework are parameters. publish-all.sh
updates the version default alongside InitRepo's, so a generated repository
cannot pin a version predating its own tooling.
The scripts are copied from tools/ by an MSBuild target when the package is
built, not duplicated into the template, so editing a script updates both.
The copy is gitignored.
InitRepo stays: it is now the path for adding the layout to a repository
that already exists, which the template cannot do.
Two packaging bugs found by installing it: ContentTargetFolders nested the
already-content-rooted paths a second time, and NuGet's default exclusion
of dotfiles dropped the .gitkeep files that create Apps/ and Libs/.
Verified end to end: install the packed template, generate Contoso, then
CreateApp, CreateAppLib and AddEntity produce a repository that builds
against the packed packages, lints clean, and serves a request with the
request-ID middleware live. Namespaces are Contoso throughout.
Adopting the layout meant either cloning this repository and copying tools/
out of it, or committing the tooling via the dotnet new template. Neither
suits someone who just wants the scaffolding available everywhere.
curl -fsSL https://raw.githubusercontent.com/MagicDoorInc/MagicCSharp/master/install.sh | bash
mcs init --prefix Acme
mcs create-app --name Shop --database shop
Installs the scripts to ~/.magiccsharp/tools and a dispatcher to
~/.magiccsharp/bin. Not ~/.magicdoor: MagicDoor's own md CLI already keeps
state there, and MagicCSharp is a framework other teams use — including,
eventually, MagicDoor's backend as one consumer among others.
Scripts now resolve templates from MAGICCSHARP_TOOLS_DIR when set, falling
back to tools/Templates, so a repository that vendored the tools keeps
working and both install modes are supported.
Update check
Once a day, in the background, printing a one-line notice on the next
command. It compares commit SHAs rather than package versions: the tools
are installed from a git ref, and scripts/version.txt tracks NuGet releases
on its own schedule — it currently says 0.0.11 while nuget.org has 0.0.13,
which would have made a version comparison lie. Never blocks a command,
only speaks on a terminal, and MAGICCSHARP_NO_UPDATE_CHECK=1 disables it.
The installer checks for SDK 10 before writing anything, since the scripts
are file-based apps and would otherwise fail confusingly on first use.
One bug worth naming: under set -euo pipefail, pending="$(cat missing |
tr ...)" aborts the dispatcher, because cat's failure propagates through
pipefail into the assignment. It only reproduced on a terminal — piped runs
returned earlier at the tty guard. Every state read now goes through one
helper that treats a missing file as empty.
Verified: install into a sandbox home, then init, create-app,
create-domain, add-entity, create-lib, sync and validate in a directory
with no tools/ of its own, producing a repository that builds. The update
check was exercised by faking a stale commit — flags in the background,
notifies on the next run, stays quiet within 24h.
The generators' templates were only reachable inside tools/, so a team that wanted its own DAL shape had no way to say so short of forking the tools. Templates now install to ~/.magiccsharp/templates, beside the scripts rather than inside them, and resolve through three layers with the first match winning: 1. .magiccsharp/templates/ in the repository, committed and shared 2. ~/.magiccsharp/templates/ installed with the tools 3. tools/Templates/ a repository that vendored the tools Resolution is per file. Overriding Entities/dal.cs.hbs leaves the other eighteen built-in and still tracking upstream, so taking one override does not mean owning all of them forever. The directory is the "templates" key of magiccsharp.json, defaulting to .magiccsharp/templates. Point it at a shared submodule, or set it to "" to turn overrides off — eject then refuses rather than writing somewhere that would be ignored. InitRepo writes the key explicitly so the mechanism is discoverable from the config rather than only from documentation. New Templates tool, exposed as `mcs templates`: list [--overridden] every template, and which layer provides it where the layers, in search order eject <path> copy a built-in in to customise Verified: eject a template, edit it, generate an entity, and the generated file carries the edit while a non-overridden template still produces the built-in output — and the result builds. A custom "templates" path is honoured, and "" disables overrides.
The exported line was hardcoded to $HOME/.magiccsharp/bin while the install location comes from MAGICCSHARP_HOME, so a custom install wrote a PATH entry for a directory that does not exist and left mcs unreachable. The "already configured" check had the same assumption and would have appended a second line on every re-run of such an install. Both now derive from the resolved bin directory, still written back through $HOME when it sits under the home directory so the profile stays portable between machines. Found by actually exercising the branch: every previous test passed NO_MODIFY_PATH=1, so the profile-writing path had never run. Now covered for zsh, bash with an existing .bash_profile, an unrecognised shell, a re-run, a custom MAGICCSHARP_HOME, and NO_MODIFY_PATH. Documented what the installer writes and where.
dotnet tool install -g MagicCSharp.Cli
mcs init --prefix Acme
The scripts worked, but each was a standalone file, so shared code had to be
copied between them: TemplateResolver lived in six files, RepoConfig in four,
about 860 of 2,593 lines duplicated. Fixing anything meant editing it six
times — the layered template resolver went in via a scripted patch, which is
the smell showing.
One project instead of eight files. The duplication is gone, and the logic
is testable for the first time: 50 tests covering template resolution,
naming, source edits and solution-file handling, where there were none.
dotnet tool also replaces machinery written by hand. install.sh — 250 lines
of bash doing shell detection, PATH editing, and a daily update check — is
deleted, along with the vendored tools/ directory. `dotnet tool update` is
the update story, and a tool manifest is the team story: pin the version in
.config/dotnet-tools.json, `dotnet tool restore`, `dotnet mcs`. The
dotnet new template now scaffolds that manifest instead of copying scripts.
Templates are embedded in the assembly rather than shipped as loose files,
so there is no path to resolve and nothing to go missing. Overrides are
unchanged — .magiccsharp/templates/ still wins, per file — and now resolve
against the built-ins rather than a third on-disk layer.
One bug worth naming, found because the tests asked for every template by
name: MSBuild infers a culture from the second-to-last extension, so
entity.cs.hbs reads as culture "cs" — Czech — and the eight *.cs.hbs
templates were routed into a cs/ satellite assembly. The build reported all
19 embedded resources; only 11 reached the DLL, and every generator that
writes C# silently produced nothing. WithCulture="false" fixes it, and a
test now asserts all 19 are present.
Verified: pack, install globally, and run init, create-app, create-domain,
add-entity, create-lib, validate and sync in an empty directory, producing a
repository that builds and serves a request. Then again through the template
and a tool manifest with `dotnet mcs`. Template overrides confirmed to apply
per file. Every command idempotent.
scripts/version.txt said 0.0.11 while nuget.org had 0.0.12 and 0.0.13 published, so the file the release script bumps from was two releases behind what consumers could actually install. A --patch release would have tried to publish 0.0.12 again. The dotnet new template pins a MagicCSharp version in the repositories it scaffolds, in both .config/dotnet-tools.json and Directory.Packages.props, and both come from one default in template.json. That sync was lost when install.sh went away, so a generated repository would have referenced whatever version happened to be hardcoded. Restored. Also adds --dry-run. The script bumps the version before it packs, so verifying that a release builds used to leave the bump and thirteen .nupkg files behind; now it restores the worktree and deletes them.
The override mechanism was described in passing — a sentence saying the directory could be "a git submodule shared across repositories" — with no instructions for actually doing it. Once a company has more than one repository that is the case that matters, because copying overrides between them means house style drifts and a fix to one never reaches the others. docs/template-overrides.md covers both: the one-off eject for a single repository, and building a template repository other repositories consume as a submodule at .magiccsharp/templates — creating it, wiring it in, the clone and CI flags a submodule needs, and how changing a shared template rolls out per repository rather than to everyone at once. Two fixes found writing it: - `mcs templates eject` printed a red "magiccsharp.json not found" before succeeding when run outside a repository. The template commands only need the config to locate the override directory, which has a default, so a missing config is ordinary rather than an error. They use a quiet load now, which is what makes it possible to build a template repository in an empty directory — the first step of the documented flow. - The documented template variables were written from memory and were wrong. Extracted them from the templates instead, per family: Repo gets version and target_framework, Apps gets database.enabled and friends, Libraries gets assembly_name, Entities gets the entity names and flags. Also converts the last `dotnet run tools/X.cs` invocations left in the guide and the template README to mcs commands.
It was displaced when the CLI section replaced the installer's, leaving the mechanism described only in a half-sentence. Also corrects the dotnet new template's description, which still said it ships a tools/ directory.
The README only carried a link to the override doc, so someone reading it front to back would not learn the mechanism exists — and it is one of the reasons to adopt the layout at all. Adds a short section: what eject does, that the copy is committed and reverting is deleting it, that resolution is per file so taking one template does not mean owning all nineteen, and the shared template repository for teams with more than one repository. The detail stays in docs/template-overrides.md. Every internal link in the README and both docs verified to resolve.
The docs stated that an override "is committed like any other file" — true, but passive, and easy to read past. Committing that directory is the whole mechanism for sharing templates with a team, so it should be an instruction rather than an observation. `mcs templates eject` now prints the git add command, at the moment the file has just been created and nothing yet suggests it is not shared. Both the README and the override guide give the command rather than describing it. It also checks whether the path is git-ignored and warns instead, because an ignored override works perfectly for whoever wrote it and reaches nobody — the kind of thing that goes unnoticed for months. Not being in a git repository at all counts as not ignored, since that is how a shared template repository starts out. Three tests: the override path eject writes to is the one the generators read back, a path outside any repository is not reported as ignored, and a missing git does not take the command down.
Three places where the framework defined half of something and left the application to supply the rest. Scheduling did not work at all. ScheduledBackgroundService resolves IScheduleStore with GetRequiredService, and the packages shipped no implementation — so "drift-free scheduling", advertised in the README, threw the moment anyone used it. Adds InMemoryScheduleStore and AddMagicScheduling(), which also registers a file-system lock provider. Both defaults are single-machine and the XML docs say exactly what that costs: two instances each keep their own schedule state and both run the job. Registering your own first wins. Nothing mapped exceptions to responses. The framework throws NotFoundException for a row that is not there, and it reached the caller as a 500 with a stack trace. AddMagicErrorHandling/UseMagicErrorHandling maps it to 404, validation and argument failures to 400, and a cancelled request to 499 rather than counting it as a server error. Outside Development an unexpected exception returns a generic message and logs the detail — its message routinely carries a connection string. Adds HttpException and the status-carrying subclasses for when a use case genuinely means a status code, while pointing at the domain exception as the better default: a use case that throws NotFoundException still works from a queue consumer. Also ValidateServices, resolving every registration at startup so a miswired dependency fails the deploy rather than the first request that needs it. Generated apps get all three wired in. One bug found by running it: ValidateServices tried to resolve keyed services, which need their key, so an ordinary app using AddOpenApi failed to boot. A preflight that stops startup on a false positive is worse than no preflight. Keyed registrations are skipped. Verified against a running generated service in Production: /probe/missing returns 404 with the entity name, /probe/boom returns a generic 500 with the connection string in the log and not the response.
Two things. MagicCSharp.App bundles the four packages a web service needs — core, AspNetCore, Events, Scheduling — behind builder.AddMagicApp() and app.UseMagicApp(builder). A generated Program.cs went from thirteen wiring lines to two, and from four package references to two. It is a shortcut, not a layer: everything it calls is public on the package that owns it, so outgrowing the defaults means replacing two lines with five. MagicAppOptions turns any piece off, and registering a transport or a schedule store first means the single-machine defaults step aside rather than fight. The granular packages remain the answer for a worker, a console app or a test project. Also fixes AddMagicCSharp, which did not forward the assembly filter its own AddMagicUseCases accepts. README: the Quick Start told people to install five packages and wire them by hand, which is now the third-best way to start and was the only one documented. It leads with the CLI, then MagicCSharp.App for an existing project, then the granular list. The repository layout was three-quarters of the way down and explained what the structure is without saying why anyone would want it. It now opens with what it buys: one repository so a cross-service change is one pull request, per-service solutions so you still build one at a time, a domain that does not know how it is stored, every entity looking the same, and conventions that are checked rather than agreed.
Package metadata. Three packages shipped RepositoryUrl=github.com/yourusername/magiccsharp in their nuspec, and the author name disagreed between packages. src/Directory.Build.props now holds the shared metadata, the version, and GenerateDocumentationFile — the XML comments were not in any of the twelve nupkgs, so none of that guidance reached IntelliSense. Symbols and SourceLink ship too. That also collapses the version bookkeeping. version.txt said 0.0.13, every csproj said 0.0.11 and the dotnet new template pinned 0.1.0, because the release script sed-ed fourteen files that could drift. It rewrites one line now, and --dry-run restores files one at a time — a single git checkout aborted on the untracked props file and silently left the rest bumped. Kafka delivery claims. The README promised "Guaranteed Delivery — persisted before returning", "Manual Commit" and at-least-once. None held: ProduceAsync's task was discarded behind a TODO, so a rejected message failed silently; EnableAutoCommit was unset, so Confluent's default committed offsets on a timer and made the listener's careful manual commit decorative. Both fixed in code — the produce is observed and failures logged, auto-commit and auto-offset-store are off. The third claim could not be fixed by config: AsyncEventDispatcher catches handler exceptions so one failure cannot stop the others, which means the transport always sees success. Both transport READMEs now say what is true — at-least-once to the dispatcher, at-most-once per handler — and what to do about it. Events README documented an instance Priority property while the dispatcher reads a static one, so every priority in the docs was silently ignored. Also adds the registration order: RegisterMagicEvents discovers handlers and must come before any transport, which alone only registers the dispatcher. Root README's Setup block had the same missing call and predates MagicCSharp.App.
CLI - create-domain --name Orders produced Apps/Shop/Shop.Domains//Default with a double slash and a wrong assembly name, then add-entity failed on the projects it did not find. The short form is now accepted and reported. - init writes a .gitignore. Without one the first build left hundreds of bin/ and obj/ files staged. - --version worked but crashed; a bad flag printed a Spectre stack trace. Parse and runtime errors now print their message and point at --help. Error handling, all found by looking at real responses - Response.ContentType was set and then overwritten by WriteAsJsonAsync, so problem responses went out as application/json. The content type goes to the write now. - The requestId in the body was Kestrel's connection counter while the header carried the real id. It comes from IRequestIdHandler, and UseRequestId now runs before the error handler so the id exists when the problem is written — and so the handler's own log line carries it. - UseExceptionHandler logged every handled exception at Error, including the 404s this module treats as routine. Replaced with explicit middleware, so there is one log line at the level the status implies. - X-Request-ID was echoed back unbounded; a 3000-character header ended up in every log line for the request. Capped at 128 and to printable ASCII. Naming, before any of it is published under these names - RegisterMagicEvents/RegisterLocalMagicEvents/RegisterMagicKafkaEvents/ RegisterMagicSQSEvents/RegisterSnowflakeKeyGen become AddMagicEvents/ AddLocalMagicEvents/AddMagicKafkaEvents/AddMagicSqsEvents/ AddSnowflakeKeyGen, matching every other registration method. The old names remain as [Obsolete] forwarders. - AddLocalMagicEvents is self-sufficient. It used to register only IEventDispatcher, so calling it alone — which the README told people to do — failed at resolution on the first dispatch. AddMagicEvents is now idempotent, so the transports calling it does not double-register. - HttpExceptions' NotFoundException and NotImplementedException collided with the domain exception and with System's under implicit usings; the latter made any file importing the namespace fail to compile. Renamed HttpNotFoundException and HttpNotImplementedException. Correctness - SearchText stripped every non-ASCII letter, so "Søgaard" indexed as "sgaard" and no one searching the actual name found it. - MagicEventSerializer keyed events by simple name via ToDictionary, so two same-named events in different namespaces crashed startup with no clue which. It now names both. CI .github/workflows/ci.yml builds, tests and lints, then does what the CHANGELOG claims and nothing automated: packs all fourteen packages, installs the CLI from that feed, scaffolds two services, a domain, two entities and a shared library from an empty directory, builds it, re-runs every command and fails if the worktree changed, then boots a service and checks it answers with a request-id header.
Building a two-service example repository with the mcs CLI, then running it
against Postgres, turned up five bugs. Each was silent: the code built, the
service started, and the wrong thing happened without a word.
Event handlers in a domain project never ran. Discovery scanned
AppDomain.CurrentDomain.GetAssemblies(), which lists only assemblies .NET has
already loaded, and it loads one lazily on first use of a type in it. A domain
project holding nothing but handlers is referenced by the host and used by
nothing, so it was not there. The dispatch returned 202 and no handler received
it. Reading the reference graph does not fix this either: the C# compiler
leaves a reference out of the compiled metadata when no type from it is used,
which is exactly that project. ApplicationAssemblies now loads the managed DLLs
deployed next to the executable, so what gets discovered follows what the
application ships. Both use-case and event-handler discovery go through it, and
the old advice to touch a type per project at startup is retired.
create-domain scaffolded a domain and left it unreferenced by the service that
owns it, so the assembly was never deployed and the above could not have saved
it anyway. It now adds the reference, as create-app already does for the data
projects.
The domain project template composed its own assembly name as
{prefix}.Libraries.{name}, leaving the service out, so two services with a
same-named domain both produced Acme.Libraries.Domains.Orders.
The HTTP layer ignored the framework's own JSON conventions. JsonDefaults was
wired into Postgres jsonb columns and nowhere else, so the same enum was a name
in the database and a number over the wire, and Optional<T> did not round-trip
through a request body — a PATCH could not tell "set this to null" from "do not
touch this", the one distinction that type exists to make. Only the two
converters are applied, for controllers and minimal APIs both; JsonDefaults
wholesale also sets IgnoreReadOnlyProperties, which suits a jsonb column and
would drop Pagination.TotalPages and every other computed property from a
response.
Also, smaller:
- GetOrThrow on IRepository. Update and Delete throw NotFoundException for a
missing key while Get returned null, so every endpoint fetching by id wrote
its own throw to get a 404 out of the error handling.
- DB_VERIFY_CONNECTION turns off the startup connection check from
configuration. Opening a connection while registering is right by default,
but there was no way to turn it off without editing the registration, which
blocked booting a service in a test that replaces every repository.
- create-domain and add-entity take a service name: --solution Shop rather than
--solution Acme.Shop.slnx, and no flag at all when the repository has one
service. More than one and no flag lists them rather than guessing.
152 tests, up from 127. The new MagicCSharp.App.Tests boots a real host; four
of its six tests failed before the JSON fix.
InsertBefore spliced the new line in at the anchor, which sits after its own leading whitespace, so the caller's eight spaces were added to the anchor's eight and the anchor was pushed onto a fresh line of stray spaces. Every generated repositories module carried it, blank lines between each registration included — and since it is the first generated code anyone reads, it is the first impression the scaffolding makes. It now reads the anchor's indentation and applies it, inserting after the last statement so the blank line separating the body from `return services;` stays where the template put it and successive entities group together.
The scaffold job commits a baseline so it can assert that re-running every mcs command leaves the worktree untouched. A runner has no git identity configured and none to derive — the account has no full name — so the commit failed with "empty ident name" and took the job with it. Passed per-command rather than configured globally: the repository it commits to is a scratch one under RUNNER_TEMP, and nothing else in the workflow commits. Both jobs run clean locally now, the whole scaffold sequence included: pack thirteen packages, install the CLI from that feed, scaffold two services with a domain, two entities and a shared library, build, re-run every command with no change, and boot the service to check the body and the X-Request-ID header.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Offers the repository layout MagicDoor runs its backend on as something other teams can adopt, with the
tooling to create and maintain it. Entirely optional — nothing in the eleven packages reads
magiccsharp.jsonor cares where a file sits, and a consumer using the packages in their own structure isunaffected by all of this.
Follows on from #1, which brought the framework improvements across and split the packages.
The gap this closes
#1 shipped
AddEntity,SyncAllProjectsandValidateConventions, and documented theApps/+Libs/layout — but nothing could create one.
AddEntityrequired projects you had to hand-build first, and theonly way to get the tools was to clone this repository and copy a directory out of it.
Three ways in
What's here
Four new generators —
InitRepo,CreateApp,CreateAppLib,CreateLib— completing the set so arepository, service, domain and shared library can all be created rather than reproduced by hand.
MagicCSharp.Cli, a .NET global tool providingmcs. Started life as eight single-filedotnet runscripts; they worked, but shared code had to be copied between them —
TemplateResolverin six files,~860 of 2,593 lines duplicated — and none of it was testable. One project instead, with 50 tests where
there were none, and
dotnet tool updatereplacing a hand-written installer and update check.MagicCSharp.Templates, adotnet newtemplate pack. The scripts are copied fromtools/by an MSBuildtarget at pack time rather than duplicated, so there is one source of truth.
Team-owned templates. Everything the generators write comes from a
.hbstemplate, and a team canreplace any single one without forking the rest. Two layers, first match winning:
.magiccsharp/templates/in the repository, then the built-ins embedded in
mcs. Configured by thetemplateskey inmagiccsharp.json;""turns overrides off.Resolution is per file, so an override taken today does not stop you receiving improvements to every
template you did not touch.
Documentation.
docs/repository-layout.mdcovers setup, what each boundary is for, what belongs inLibs/and what does not, overriding templates, and adopting this in a repository that already exists.tools/README.mdgives every tool its real options and a worked example.Review notes
Three bugs here were only findable by running the thing end to end, and are the parts most worth a look:
Directory.Build.propsapplied to the tools themselves, so a repo targeting net9.0 producedscripts the .NET 10 runtime refused to launch.
tools/Directory.Build.propsnow pins them and does notinherit.
entity.cs.hbsreads as culturecs—Czech — and the eight
*.cs.hbstemplates were routed into acs/satellite assembly. The build reportedall 19 embedded resources; only 11 reached the DLL, and every generator that writes C# silently produced
nothing.
WithCulture="false"fixes it, and a test now asserts all 19 are present.dotnet toolmade moot:set -euo pipefailkilling thedispatcher on a missing state file, and a PATH line hardcoded away from the real install location. Both
are gone with
install.sh.Two deliberate calls worth confirming:
consume them. A new repository should not start a version behind.
and
scripts/version.txtmoves on the NuGet release schedule.Verified
From an empty directory, running only the documented commands:
init→create-app→create-domain→create-lib→add-entity. The result builds against the packed packages, passesValidateConventions, anda generated service serves
GET /hellowith anX-Request-IDheader confirming the middleware is wired.Repeated through all three entry points, including with no local
tools/directory.Template overrides verified by ejecting one, editing it, and confirming the generated entity carries the edit
while a non-overridden template still produces built-in output. The installer's PATH handling covered for
zsh, bash, an unrecognised shell, a re-run, a custom
MAGICCSHARP_HOME, andNO_MODIFY_PATH. Every tool isidempotent — second runs produce no diff.
Framework unchanged: build clean, 47 tests passing, linter clean across 114 files, all 12 packages pack.
Not in this PR
AddEvent,AddLibandGenerateAssemblyCatalogare still unported. The last matters once a service spansmany projects — .NET loads an assembly only when one of its types is first touched, so use cases in a project
the host never references go unregistered.
Separately, and not addressed here:
scripts/version.txtsays0.0.11while nuget.org has0.0.12and0.0.13published.