Skip to content

feat(csharp): derive C# namespace from metadata packages by default - #512

Merged
dmealing merged 6 commits into
mainfrom
fm/mo-csharp-ns-default
Oct 30, 2026
Merged

dmealing merged 6 commits into
mainfrom
fm/mo-csharp-ns-default

Conversation

@dmealing

@dmealing dmealing commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • C# codegen now derives each type's namespace from its metadata package when nothing is configured. Each :: segment is PascalCased and joined with ., so acme::billing becomes Acme.Billing. Implemented as PackageBindingResolver.DefaultConvention, applied only when no Convention and no flat namespace are set.
  • Types that belong to no single package (AppDbContext, routes) land in the common root of the model's packages. PackageBindingResolver.WithDerivedHome computes it per run, and CodegenRunner applies it. A model whose packages share no root still fails, with a message that names the setting.
  • csharp.namespace and --namespace still set one flat namespace and replace the derived default. Generators that emit into the home namespace add using directives for types resolved elsewhere (HomeUsings). Changes are covered by the new DerivedNamespaceTests.cs and updated namespace tests, and by regenerated showcase output.
Validation

Risk Assessment

✅ Low: Default package-derived C# namespace with a derived home is implemented, now filters to in-scope packages, emits usings for cross-package references, and tests exercise zero-config, override, dependency-import and multi-package compile cases.

Testing

Baseline ts-fast/ts-unit green. Drove six live CLI scenarios with the branch-built dotnet meta gen in disposable projects: single-package default, multi-package default, out-of-scope dependency default, a dependency with a different root, the no-shared-root guard, and explicit namespace override. All behaved as the intent requires. Multi-package generated output compiles as a library with 0 errors. Full C# solution suite passed. Not live: report-export, TPH routes, shared Enums, output prompt/parser, extractor, callable and render helper were not generated or compiled live. They rely on unit tests only.</testing_summary> <artifacts>[{"label":"Live gen transcripts for the six C# namespace scenarios","kind":"log","path":"/csharp-default-namespace-gen-transcripts.txt"}]

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Single package, nothing configured: gen places types in the package-derived namespace (Acme.Shop) ✅ pass live transcript s1: Subscriber.g.cs namespace Acme.Shop, exit 0
Multi-package model (acme::billing, acme::crm), nothing configured: per-package namespaces plus shared home Acme ✅ pass live transcript s2, exit 0
Multi-package output compiles with no namespace configured (entity, names, DbContext, allowlist, routes) ✅ pass live scratch Compile.csproj Build succeeded, 0 errors over s2 Generated/
Out-of-scope dependency does not block home derivation (multi-package plus acme-common, nothing configured) ✅ pass live transcript s3, exit 0
Out-of-scope dependency with a different root (app::orders + acme::common): home derived from in-scope package only ✅ pass live transcript s6, all artifacts in App.Orders, exit 0
Adversarial guard: in-scope packages with no shared root and nothing configured fail with the named setting error ✅ pass live transcript s4, exit 1 naming csharp.namespace and --namespace
Explicit csharp.namespace overrides derivation for every generated file ✅ pass live transcript s5, all files Acme.Blog, exit 0
Other home-namespace generators (report-export, TPH routes, shared Enums, output prompt/parser, extractor, callable, render helper) compile in a multi-package no-namespace model ⏸️ untested no Only entity, names, DbContext, allowlist and routes were generated and compiled live. The other generators need metadata I did not build (report, TPH, template.prompt/output, stored proc, shared enum)…
Full C# regression suite ✅ pass live dotnet test MetaObjects.sln: Render 364, Conformance 2195, Codegen 1086 (1 skipped), Cli 345, all passed
Evidence: Live gen transcripts for the six C# namespace scenarios
\### s1 single package acme::shop, nothing configured
  unchanged: SubscriberRoutes.g.cs
dotnet meta gen: 0 file(s) written
exit=0
namespace Acme.Shop;
\### s2
dotnet meta: running the generator list in codegen/Generators.cs — `dotnet run --project codegen -- gen`.
  written: Invoice.g.cs
  written: Lead.g.cs
  written: AppDbContext.g.cs
  written: InvoiceNames.g.cs
  written: LeadNames.g.cs
  written: InvoiceFilterAllowlist.g.cs
  written: LeadFilterAllowlist.g.cs
  written: InvoiceRoutes.g.cs
  written: LeadRoutes.g.cs
dotnet meta gen: 9 file(s) written
\### s3
dotnet meta: running the generator list in codegen/Generators.cs — `dotnet run --project codegen -- gen`.
  written: Invoice.g.cs
  written: Lead.g.cs
  written: AppDbContext.g.cs
  written: InvoiceNames.g.cs
  written: LeadNames.g.cs
  written: InvoiceFilterAllowlist.g.cs
  written: LeadFilterAllowlist.g.cs
  written: InvoiceRoutes.g.cs
  written: LeadRoutes.g.cs
dotnet meta gen: 9 file(s) written
\### s4
dotnet meta: running the generator list in codegen/Generators.cs — `dotnet run --project codegen -- gen`.
  error: codegen failed: no C# namespace for the generated code: set "csharp": { "namespace": "<ns>" } in .metaobjects/config.json or pass --namespace <ns> (the metadata packages share no common root to derive one from).
dotnet meta gen: FAILED (a generator threw — see the error above)
\### s5
dotnet meta: running the generator list in codegen/Generators.cs — `dotnet run --project codegen -- gen`.
  written: Invoice.g.cs
  written: Lead.g.cs
  written: AppDbContext.g.cs
  written: InvoiceNames.g.cs
  written: LeadNames.g.cs
  written: InvoiceFilterAllowlist.g.cs
  written: LeadFilterAllowlist.g.cs
  written: InvoiceRoutes.g.cs
  written: LeadRoutes.g.cs
dotnet meta gen: 9 file(s) written
\### s6
dotnet meta: running the generator list in codegen/Generators.cs — `dotnet run --project codegen -- gen`.
  written: Order.g.cs
  written: AppDbContext.g.cs
  written: OrderNames.g.cs
  written: OrderFilterAllowlist.g.cs
  written: OrderRoutes.g.cs
dotnet meta gen: 5 file(s) written
\### s2 compile (Generated/ compiled as library with EF Core + routes runtime)
dotnet build -> Build succeeded, 0 errors
\### generated namespaces s2 (nothing configured)
Generated/AppDbContext.g.cs: namespace Acme;
Generated/InvoiceFilterAllowlist.g.cs: namespace Acme;
Generated/Invoice.g.cs: namespace Acme.Billing;
Generated/InvoiceNames.g.cs: namespace Acme.Billing;
Generated/InvoiceRoutes.g.cs: namespace Acme;
Generated/LeadFilterAllowlist.g.cs: namespace Acme;
Generated/Lead.g.cs: namespace Acme.Crm;
Generated/LeadNames.g.cs: namespace Acme.Crm;
Generated/LeadRoutes.g.cs: namespace Acme;

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (3) ✅
  • 🚨 server/csharp/MetaObjects.Codegen/Generators/RoutesGenerator.cs:290 - Default per-package namespaces put routes in the shared home namespace, but they emit no using for the entity's package namespace. A multi-package model with nothing configured now generates C# that does not compile. Example: packages acme::billing and acme::crm, no csharp.namespace. Invoice goes to Acme.Billing; InvoiceRoutes.g.cs goes to Acme and references Invoice and InvoiceNames.* with only framework usings. Result: CS0246 in the adopter's build, while gen exits 0. Before this change that split layout needed explicit PackageNamespaces/Convention; now it is the default for every multi-package model. DerivedNamespaceTests.A_multi_package_model_compiles compiles only Entity, DbContext and FilterAllowlist, so it skips the broken tier. Remedy: emit using for the entity's resolved namespace (as the ReverseFinders hunk does) or emit per-entity artifacts into the entity namespace, then add Routes, ReportExport and the other home-namespace generators to the compile test. Same invariant at: RoutesGenerator.cs:600 (TPH routes, same missing using); ReportExportGenerator.cs:103,138 (export class in home ns, row type in package ns); EntityGenerator.cs:969 (shared Enums.g.cs in home ns); CSharpNaming.cs:1131 (QualifiedOwnerTypeRef uses global::{config.Namespace}.{owner}, wrong for a package-namespaced owner); OutputPromptGenerator.cs:68, OutputParserGenerator.cs:113 and ExtractorGenerator.cs:73,138 (home ns; need a check that their entity/payload references resolve).

🔧 Fix applied.
2 warnings still open:

  • ⚠️ server/csharp/MetaObjects.Codegen/PackageBindingResolver.cs:38 - WithDerivedHome computes the home namespace from every root-level object (root.Objects()), including objects imported from metadata dependencies (FR-023). CodegenRunner only generates for root.InScope(o) objects (CodegenRunner.cs:66,77). With nothing configured, a project in acme::shop that imports a dependency in vendor::lib gets no common prefix. DerivedHome stays unset, and AppDbContext, routes, filter allowlists, shared Enums.g.cs and the other home-namespace artifacts throw "no C# namespace ... (the metadata packages share no common root ...)". The failure is caused by objects the project does not generate. This is the exact zero-config case the intent targets. Remedy: filter the package set with root.InScope(o), matching the runner. Same invariant at the call site CodegenRunner.cs:48 (the only caller that passes the full root).
  • ⚠️ server/csharp/MetaObjects.Codegen.Tests/DerivedNamespaceTests.cs:126 - The previous round's instruction was to add every home-namespace generator to a multi-package, no-namespace compile test. A_multi_package_model_compiles still compiles only Entity, DbContext, FilterAllowlist, Names and ReportExport, with Routes excluded (checked by parsing its usings instead). OutputParser, OutputPrompt, Extractor, Callable, RenderHelper and the shared Enums.g.cs (a shared field.enum consumed from a package namespace) have no multi-package fixture. They resolve today because they use global::-qualified refs or the enclosing-namespace lookup from the derived home. A regression in any of them would ship as CS0246 while gen exits 0. Remedy: add a template.prompt with @responseRef/@payloadRef, a stored-proc callable and a shared enum in the second package to the fixture, and include those generators in the compile list.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ server/csharp/MetaObjects.Codegen/PackageBindingResolver.cs:38 - WithDerivedHome computes the home namespace from every root-level object (root.Objects()), including objects imported from metadata dependencies (FR-023). CodegenRunner only generates for root.InScope(o) objects (CodegenRunner.cs:66,77). With nothing configured, a project in acme::shop that imports a dependency in vendor::lib gets no common prefix. DerivedHome stays unset, and AppDbContext, routes, filter allowlists, shared Enums.g.cs and the other home-namespace artifacts throw "no C# namespace ... (the metadata packages share no common root ...)". The failure is caused by objects the project does not generate. This is the exact zero-config case the intent targets. Remedy: filter the package set with root.InScope(o), matching the runner. Same invariant at the call site CodegenRunner.cs:48 (the only caller that passes the full root).
  • ⚠️ server/csharp/MetaObjects.Codegen/PackageBindingResolver.cs:38 - Round 2 fix not applied. WithDerivedHome still reads root.Objects(), including objects imported from metadata dependencies (FR-023), while CodegenRunner generates only root.InScope(o) objects (CodegenRunner.cs:66,77). The round-2 commit (48d03ec) touched only DerivedNamespaceTests.cs and left this source unchanged, despite its message. With nothing configured, a project in acme::shop importing vendor::lib gets no common prefix. DerivedHome stays unset, and AppDbContext, routes, filter allowlists, shared Enums.g.cs and the other home-namespace artifacts throw "no C# namespace ... (the metadata packages share no common root ...)". The failure comes from objects the project does not generate, in the zero-config case the intent targets. Remedy: filter with .Where(root.InScope) before EffectivePackage. The fixture has no dependency import, so no test catches this; add one with an out-of-scope package in a different root.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 9 scenarios driven live against the product
Scenario Result Live Evidence
Single package, nothing configured: gen places types in the package-derived namespace (Acme.Shop) ✅ pass live transcript s1: Subscriber.g.cs namespace Acme.Shop, exit 0
Multi-package model (acme::billing, acme::crm), nothing configured: per-package namespaces plus shared home Acme ✅ pass live transcript s2, exit 0
Multi-package output compiles with no namespace configured (entity, names, DbContext, allowlist, routes) ✅ pass live scratch Compile.csproj Build succeeded, 0 errors over s2 Generated/
Out-of-scope dependency does not block home derivation (multi-package plus acme-common, nothing configured) ✅ pass live transcript s3, exit 0
Out-of-scope dependency with a different root (app::orders + acme::common): home derived from in-scope package only ✅ pass live transcript s6, all artifacts in App.Orders, exit 0
Adversarial guard: in-scope packages with no shared root and nothing configured fail with the named setting error ✅ pass live transcript s4, exit 1 naming csharp.namespace and --namespace
Explicit csharp.namespace overrides derivation for every generated file ✅ pass live transcript s5, all files Acme.Blog, exit 0
Other home-namespace generators (report-export, TPH routes, shared Enums, output prompt/parser, extractor, callable, render helper) compile in a multi-package no-namespace model ⏸️ untested no Only entity, names, DbContext, allowlist and routes were generated and compiled live. The other generators need metadata I did not build (report, TPH, template.prompt/output, stored proc, shared enum)…
Full C# regression suite ✅ pass live dotnet test MetaObjects.sln: Render 364, Conformance 2195, Codegen 1086 (1 skipped), Cli 345, all passed
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • cd server/csharp && dotnet build MetaObjects.Cli -c Release -o <tmp>/cli (branch CLI built into a temp dir outside the worktree)
  • dotnet <tmp>/cli/MetaObjects.Cli.dll init then gen in disposable projects; codegen/Codegen.csproj ProjectReferences the worktree MetaObjects.Codegen
  • s1: package acme::shop, nothing configured -> Subscriber.g.cs namespace Acme.Shop, exit 0
  • s2: packages acme::billing + acme::crm, nothing configured -> Acme.Billing, Acme.Crm, shared files in Acme, exit 0
  • s2 compile: scratch Sdk.Web library csproj over Generated/**/*.cs with ProjectReference to worktree MetaObjects, MetaObjects.Render, MetaObjects.Codegen -> Build succeeded, 0 errors
  • s3: acme::billing + acme::crm plus dependency acme-common (acme::common, out of scope), nothing configured -> exit 0
  • s6: entity app::orders plus dependency acme::common, nothing configured -> exit 0, all artifacts in App.Orders
  • s4 guard: alpha::x and beta::y, nothing configured -> exit 1 with the named csharp.namespace / --namespace error
  • s5: acme::billing + acme::crm with csharp.namespace Acme.Blog -> all files Acme.Blog, exit 0
  • Transcripts: <evidence-dir>/csharp-default-namespace-gen-transcripts.txt
  • cd server/csharp && dotnet test MetaObjects.sln -> Render 364, Conformance 2195, Codegen 1086 (1 skipped), Cli 345, all passed
  • git status in worktree after testing: clean
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

… Two stale spots fixed: the port doc paragraph (added the no-common-root failure, rewrapped) and the codegen skill reference, which now points to docs/ports/csharp.md. CHANGELOG and docs/features/cli.md were already updated by the change. No executable code or tests touched.
@dmealing
dmealing merged commit beb009b into main Oct 30, 2026
9 checks passed
@dmealing
dmealing deleted the fm/mo-csharp-ns-default branch October 30, 2026 06:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant