Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,15 @@ here.**
committed output, so output generated under another namespace is drift. A C# namespace comes
from the package bindings (`GenConfig.TypeOverrides`, then `PackageNamespaces`, then the
`Convention` rule, then `UnmappedStrategy`, each keyed by an object's own metadata package);
the default `Flatten` strategy places every type in one namespace, `--namespace` or else the
new `csharp.namespace` key in `.metaobjects/config.json`. There is no default value: the
constant `Generated` is gone, and a run that emits a file needing the namespace without one
set stops and asks for it (a run that emits nothing never does). A namespace that is not a
dotted path of C# identifiers fails generation. **Upgrade step:** set
`"csharp": { "namespace": "<ns>" }` to the namespace your committed output uses (`Generated`
if you never passed `--namespace`), so `gen` and a bare `dotnet meta verify` both use it. `dotnet meta gen`, `verify --codegen`, `init`
with nothing configured, each type's namespace is derived from its own metadata package (each
`::` segment PascalCased, joined with `.`: `acme::billing` is `Acme.Billing`), and the types that
belong to no single package (`AppDbContext`, routes) land in the namespace every package shares.
`csharp.namespace` in `.metaobjects/config.json` (or `--namespace`) replaces the derived default
with one flat namespace for types no binding places. A namespace that is not a dotted path of C#
identifiers fails generation, naming the metadata package it came from. **Upgrade step:** a
project that set no namespace now gets namespaces derived from its packages instead of
`Generated`; to keep the namespace you generated with before, set
`"csharp": { "namespace": "Generated" }` (or whatever your committed output uses). `dotnet meta gen`, `verify --codegen`, `init`
and `eject` no longer check whether `codegen/Program.cs` runs `Generators.All`.
**Upgrade step:** if `codegen/Program.cs` builds its own generator list, replace that list
with `Generators.All`. Such a project still runs its own list and ignores
Expand Down
6 changes: 3 additions & 3 deletions agent-context/skills/metaobjects-codegen/references/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ set and no separate docs command. `--generators <names>` narrows a run to entrie
list already declares, by stable name; it never adds one, and naming an entry the list
does not declare is refused. `dotnet meta gen --list` is the catalog of what the port
registers, not what this project runs; under each name it prints the expression that lists
that generator in `codegen/Generators.cs` and its options. Output lands under `--namespace` in
`--out` unless a generator's own init properties say otherwise — documentation
does (below).
that generator in `codegen/Generators.cs` and its options. Output lands in `--out`, in the
namespace its metadata package maps to (see `docs/ports/csharp.md`), unless a generator's own
init properties say otherwise — documentation does (below).

| Stable name | Entry in `codegen/Generators.cs` (`new MetaObjects.Codegen.Generators.…`) | Output |
|---|---|---|
Expand Down
6 changes: 3 additions & 3 deletions docs/features/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ the committed `--out` tree, never touching it), and a **clean `--db` rejection
directory and `codegen/Generators.cs` are present, `--codegen`. The output directory is
`csharp.outDir` in `.metaobjects/config.json` (the same setting `gen` writes to); `--out <dir>`
wins over it. With neither, the codegen gate is skipped with a note that names the setting. The
namespace for types no package binding places is `--namespace`, else `csharp.namespace` there,
read the same way by `gen` and `verify`; a run that emits such a type with neither set stops and
asks for one. A bare run prints the subverb note. The **Java/Kotlin `mvn metaobjects:verify`** port expresses the same vocabulary as a
namespace of each type comes from its metadata package (`acme::billing` is `Acme.Billing`);
`csharp.namespace` there (or `--namespace`) replaces that with one flat namespace for types no
package binding places, read the same way by `gen` and `verify`. A bare run prints the subverb note. The **Java/Kotlin `mvn metaobjects:verify`** port expresses the same vocabulary as a
`mode` parameter (Maven goals are parameter-driven, not flag-driven). With `mode` unset the goal
runs both gates below, skipping the template gate with a note when no `templateRoot` is set.
`-Dmeta.verify.mode=codegen`
Expand Down
14 changes: 8 additions & 6 deletions docs/ports/csharp.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,14 @@ directory holding `.metaobjects/`:
Each generated type's namespace comes from the package bindings, keyed by that type's own
metadata package: a type-level override (`GenConfig.TypeOverrides`), then a package-level one
(`PackageNamespaces`), then the `Convention` rule (strip a package prefix, prepend a root, case
each `::` segment), then `UnmappedStrategy`. The default strategy, `Flatten`, puts every type in
one namespace: `--namespace` when given, else `csharp.namespace`. There is no built-in value, so
a run that emits a file needing it with neither set stops and names both; a run that emits
nothing never asks. The namespace must be a dotted path of C# identifiers. `dotnet meta gen` and
`verify` set only that flat namespace; the per-package bindings are `GenConfig` properties, set
when you run codegen from your own code with `CodegenRunner`.
each `::` segment), then `UnmappedStrategy`. With none of those configured, the default
convention applies: each `::` segment PascalCased and joined with `.` (`acme::billing` is
`Acme.Billing`), and the artifacts that belong to no single package (`AppDbContext`, routes) land in
the namespace all the packages share. When they share no root, generation stops and asks for a
namespace. Setting `--namespace` or `csharp.namespace` switches the default off and puts every type
no binding places in that one flat namespace. The namespace must be a dotted path of C#
identifiers. `dotnet meta gen` and `verify` set only that flat namespace; the per-package bindings
are `GenConfig` properties, set when you run codegen from your own code with `CodegenRunner`.

With that, `dotnet meta gen` writes there, `dotnet meta verify --codegen` diffs against it, and
a plain `dotnet meta verify` in CI runs the codegen gate with no flags. `--out <dir>` still works
Expand Down
12 changes: 6 additions & 6 deletions examples/showcase/.metaobjects/.gen-state/.hashes.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"generated/csharp/AppDbContext.g.cs": "314c6078030ba151687430b37e6b6ea936238bb5df02df238e4acce2f95a430d",
"generated/csharp/Subscriber.g.cs": "3ca160941378fbb8b6ad6acf080ff85573f463931e21b8756873a4476e5dd163",
"generated/csharp/SubscriberBlurbPayload.g.cs": "6b1ce5b5a4018c647586d86125718c4764ae5d6eeb94e9545e8006a7ec2b2b6f",
"generated/csharp/SubscriberFilterAllowlist.g.cs": "7428d47f6ccb64bb315266ee0564fa3803736623a34ab491435c042848c5d472",
"generated/csharp/SubscriberNames.g.cs": "91f6eba78657b31fceef7021c6ca6bd799524dcb5120acbc8fcbe18786787174",
"generated/csharp/SubscriberRoutes.g.cs": "acb153e2aac648a487d78d6d5321c0f250451f47b691f988e3b99ff410bcfa06",
"generated/csharp/AppDbContext.g.cs": "4d441b4283d074cd98d11385bd8c9a2294a9378d3b392b451aea3130b5d2ca28",
"generated/csharp/Subscriber.g.cs": "a7701fda82e3eb27dad5a01f326c0893ecd2ac418eea6e3afc5c4a2b81535bc2",
"generated/csharp/SubscriberBlurbPayload.g.cs": "29eb3a6ce64496ffe734cba55d0213994316b4281d3fa6791917393bae36c9ed",
"generated/csharp/SubscriberFilterAllowlist.g.cs": "3839b04884e4e393d4f7d7bf4c7d4fe5b3852bd3a92b1ecb282cb8935a3c98e5",
"generated/csharp/SubscriberNames.g.cs": "c2cf0248c2ecde155ec2623604771e39bed23783603d6f18a2ed82967227a732",
"generated/csharp/SubscriberRoutes.g.cs": "15450e230d396a739d789340c42488da354c22265193b5ac61b074b0ba6fb1ff",
"generated/python/Subscriber.py": "9dd4c0acea47f99d1daf79930f9de2e400f30780ebce6c63f023be528a72b762",
"generated/python/SubscriberBlurbPayload.py": "a5ef8a08e63efbe0f73e39a21a368cff5150c68cc8beeea6e8aab0c755f8bbdd",
"generated/python/__init__.py": "daa04cec586768866d0996c779f62073e6773fe63750ff668327991fac2fd0f5",
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/generated/csharp/AppDbContext.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#nullable enable
using Microsoft.EntityFrameworkCore;

namespace Generated;
namespace Acme;

public class AppDbContext : DbContext
{
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/generated/csharp/Subscriber.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Generated;
namespace Acme;

[Table(SubscriberNames.SourcePrimaryTable)]
public class Subscriber
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;

namespace Generated;
namespace Acme;

public class SubscriberBlurbPayload
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#nullable enable
using System.Collections.Generic;

namespace Generated;
namespace Acme;

/// <summary>
/// GENERATED — per-entity FR-009 filter allowlist for Subscriber.
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/generated/csharp/SubscriberNames.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#nullable enable
using System.Collections.Generic;

namespace Generated;
namespace Acme;

/// <summary>
/// GENERATED — per-object physical database names for Subscriber (spec A1/A2/A6).
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/generated/csharp/SubscriberRoutes.g.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Microsoft.EntityFrameworkCore;
using MetaObjects.Codegen.Runtime;

namespace Generated;
namespace Acme;

public static class SubscriberRoutes
{
Expand Down
2 changes: 1 addition & 1 deletion examples/showcase/site-payload.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"csharp-entity": {
"lang": "csharp",
"inline": "<span class=\"comment\">// &lt;auto-generated/&gt;</span>\n…\n[<span class=\"comment\">Table(SubscriberNames.SourcePrimaryTable)</span>]\n<span class=\"keyword\">public</span> <span class=\"keyword\">class</span> <span class=\"key\">Subscriber</span>\n{\n <span class=\"keyword\">public</span> <span class=\"keyword\">enum</span> SubscriberStatus { active, paused, cancelled }\n [<span class=\"comment\">Key</span>]\n [<span class=\"comment\">Column(SubscriberNames.IdColumn)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">long</span> Id { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.EmailColumn)</span>]\n [<span class=\"comment\">Required(AllowEmptyStrings = true)</span>]\n [<span class=\"comment\">MaxLength(320)</span>]\n…\n <span class=\"keyword\">public</span> <span class=\"keyword\">string</span> Email { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; } = <span class=\"keyword\">default</span>!;\n [<span class=\"comment\">Column(SubscriberNames.NameColumn)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">string</span>? Name { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.StatusColumn)</span>]\n <span class=\"keyword\">public</span> SubscriberStatus Status { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.CreatedAtColumn)</span>]\n <span class=\"keyword\">public</span> DateTimeOffset? CreatedAt { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n}",
"full": "<span class=\"comment\">// &lt;auto-generated/&gt;</span>\n<span class=\"comment\">// Generated by MetaObjects entity-generator. Do not edit by hand.</span>\n<span class=\"comment\">#nullable enable</span>\n<span class=\"keyword\">using</span> System;\n<span class=\"keyword\">using</span> System.Collections.Generic;\n<span class=\"keyword\">using</span> System.ComponentModel.DataAnnotations;\n<span class=\"keyword\">using</span> System.ComponentModel.DataAnnotations.Schema;\n\n<span class=\"keyword\">namespace</span> <span class=\"key\">Generated</span>;\n\n[<span class=\"comment\">Table(SubscriberNames.SourcePrimaryTable)</span>]\n<span class=\"keyword\">public</span> <span class=\"keyword\">class</span> <span class=\"key\">Subscriber</span>\n{\n <span class=\"keyword\">public</span> <span class=\"keyword\">enum</span> SubscriberStatus { active, paused, cancelled }\n [<span class=\"comment\">Key</span>]\n [<span class=\"comment\">Column(SubscriberNames.IdColumn)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">long</span> Id { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.EmailColumn)</span>]\n [<span class=\"comment\">Required(AllowEmptyStrings = true)</span>]\n [<span class=\"comment\">MaxLength(320)</span>]\n [<span class=\"comment\">MinLength(1)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">string</span> Email { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; } = <span class=\"keyword\">default</span>!;\n [<span class=\"comment\">Column(SubscriberNames.NameColumn)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">string</span>? Name { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.StatusColumn)</span>]\n <span class=\"keyword\">public</span> SubscriberStatus Status { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.CreatedAtColumn)</span>]\n <span class=\"keyword\">public</span> DateTimeOffset? CreatedAt { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n}\n",
"full": "<span class=\"comment\">// &lt;auto-generated/&gt;</span>\n<span class=\"comment\">// Generated by MetaObjects entity-generator. Do not edit by hand.</span>\n<span class=\"comment\">#nullable enable</span>\n<span class=\"keyword\">using</span> System;\n<span class=\"keyword\">using</span> System.Collections.Generic;\n<span class=\"keyword\">using</span> System.ComponentModel.DataAnnotations;\n<span class=\"keyword\">using</span> System.ComponentModel.DataAnnotations.Schema;\n\n<span class=\"keyword\">namespace</span> <span class=\"key\">Acme</span>;\n\n[<span class=\"comment\">Table(SubscriberNames.SourcePrimaryTable)</span>]\n<span class=\"keyword\">public</span> <span class=\"keyword\">class</span> <span class=\"key\">Subscriber</span>\n{\n <span class=\"keyword\">public</span> <span class=\"keyword\">enum</span> SubscriberStatus { active, paused, cancelled }\n [<span class=\"comment\">Key</span>]\n [<span class=\"comment\">Column(SubscriberNames.IdColumn)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">long</span> Id { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.EmailColumn)</span>]\n [<span class=\"comment\">Required(AllowEmptyStrings = true)</span>]\n [<span class=\"comment\">MaxLength(320)</span>]\n [<span class=\"comment\">MinLength(1)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">string</span> Email { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; } = <span class=\"keyword\">default</span>!;\n [<span class=\"comment\">Column(SubscriberNames.NameColumn)</span>]\n <span class=\"keyword\">public</span> <span class=\"keyword\">string</span>? Name { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.StatusColumn)</span>]\n <span class=\"keyword\">public</span> SubscriberStatus Status { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n [<span class=\"comment\">Column(SubscriberNames.CreatedAtColumn)</span>]\n <span class=\"keyword\">public</span> DateTimeOffset? CreatedAt { <span class=\"keyword\">get</span>; <span class=\"keyword\">set</span>; }\n}\n",
"lineCount": 29
},
"java-dto": {
Expand Down
16 changes: 7 additions & 9 deletions server/csharp/MetaObjects.Cli.Tests/CodegenNamespaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
namespace MetaObjects.Cli.Tests;

/// <summary>
/// The flat namespace gen and verify --codegen generate under: <c>--namespace</c>, else
/// <c>csharp.namespace</c> in <c>.metaobjects/config.json</c>; with neither, only a run that emits
/// a file needing one stops. Driven through the entry point a project's
/// The namespace gen and verify --codegen generate under: <c>--namespace</c>, else
/// <c>csharp.namespace</c> in <c>.metaobjects/config.json</c>; with neither, each type's namespace is
/// derived from its metadata package. Driven through the entry point a project's
/// codegen/Program.cs calls (<see cref="OwnedRunner"/>); the oracle is the
/// <c>namespace ...;</c> declaration in the generated file, the output contract an adopter's code
/// compiles against.
Expand Down Expand Up @@ -65,18 +65,16 @@ public void A_config_namespace_is_used_by_gen_and_a_bare_verify_is_clean()
}

[Fact]
public void With_no_namespace_a_run_that_emits_code_stops_and_asks_for_one()
public void With_no_namespace_it_is_derived_from_the_metadata_package_and_a_bare_verify_is_clean()
{
WriteConfig("\"outDir\": \"Generated\"");

var gen = Run("gen");
Assert.NotEqual(0, gen.Exit);
Assert.Contains("\"csharp\": { \"namespace\": \"<ns>\" }", gen.Output);
Assert.False(File.Exists(Path.Combine(OutDir, "Subscriber.g.cs")));
Assert.True(gen.Exit == 0, gen.Output);
Assert.Contains("namespace Acme.Shop;", EntityFile());

var verify = Run("verify", "--codegen");
Assert.Equal(2, verify.Exit);
Assert.Contains("\"csharp\": { \"namespace\": \"<ns>\" }", verify.Output);
Assert.True(verify.Exit == 0, verify.Output);
}

[Fact]
Expand Down
16 changes: 14 additions & 2 deletions server/csharp/MetaObjects.Codegen.Tests/DependencyCodegenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@ private void Write(string rel, string content)
File.WriteAllText(path, content);
}

private Dictionary<string, string> GenerateAll()
private Dictionary<string, string> GenerateAll(string? ns = "Acme.Generated")
{
var load = MetadataLocation.Resolve(null, _tmp).Load();
Assert.Empty(load.Errors);
// The import is on the loaded root: that is what the generators read.
Assert.NotNull(load.Root.ImportScope);
var config = new GenConfig { OutDir = OutDir, Namespace = "Acme.Generated" };
var config = ns is null ? new GenConfig { OutDir = OutDir } : new GenConfig { OutDir = OutDir, Namespace = ns };
var names = GeneratorRegistry.Entries.Keys.Where(n => !Excluded.Contains(n)).ToList();
CodegenRunner.Run(config, load.Root, GeneratorRegistry.Resolve(names));
return Directory.EnumerateFiles(OutDir, "*", SearchOption.AllDirectories)
Expand Down Expand Up @@ -94,6 +94,18 @@ public void An_imported_node_emits_nothing_from_any_registered_generator()
}
}

[Fact]
public void With_no_namespace_configured_an_imported_package_does_not_decide_the_home_namespace()
{
Project();
var output = GenerateAll(ns: null);

// The project's own package `app` and the imported `acme::common` share no root; only the
// project's own package is generated, so only it names the home namespace.
Assert.Contains("namespace App;", output["AppDbContext.g.cs"]);
Assert.Contains("namespace App;", output["Order.g.cs"]);
}

[Fact]
public void An_imported_value_object_a_local_entity_holds_is_still_emitted()
{
Expand Down
Loading
Loading