Summary
Make EcoreNetto self-hosting: read TestData/ecore.ecore (the self-describing Ecore meta-metamodel) with EcoreNetto's own reader and run Handlebars templates to regenerate EcoreNetto's own ModelElement/** classes — mirroring how the sibling project uml4net regenerates itself from UML.xmi.
This issue captures a researched implementation plan to come back to later. Not yet scheduled.
Background — how uml4net does it (reference design)
- A dedicated tooling library
uml4net.CodeGenerator (not shipped, IsPackable=false) reads UML.xmi into an in-memory graph, then Handlebars templates emit the C# POCO interfaces/classes/enums (and separate XMI readers/writers) that make up uml4net itself — a closed self-generating loop.
- Generator hierarchy:
Generator (Roslyn Formatter.Format cleanup + UTF-8 write) → HandleBarsGenerator (shared Handlebars env, RegisterHelpers/RegisterTemplates) → UmlHandleBarsGenerator → CorePocoGenerator/XmiReaderGenerator/XmiWriterGenerator. A ModelTransformer runs first to fix C#-illegal names.
- Templates are thin (copyright header + "AUTOGENERATED FILE" banner + fixed usings), delegating all logic to C# Handlebars helpers.
- Generation is driven by NUnit, not a CLI: normal tests generate into a scratch dir and byte-compare against committed golden files under
Expected/AutoGen*/; a separate [Explicit("Regenerates ... production code")] test writes directly into the real source dirs.
What EcoreNetto already has
- A reader:
ReportGenerator.LoadRootPackage(FileInfo) → ResourceSet.CreateResource → Resource.Load → ECoreParser.ParseXml produces the EPackage object graph. ecore.ecore is already test data.
- A Handlebars stack:
ECoreNetto.Reporting/Generators/HandleBarsReportGenerator.cs (+ CreateHandlebarsPayload tree-flatten).
- C#-oriented helpers:
ECoreNetto.HandleBars/GeneralizationHelper.cs (Generalization.Classes → : FirstSuper, ISelf, Generalization.Interfaces → : IFoo, IBar), plus StructuralFeatureHelper, StringHelper, BooleanHelper (defined but unregistered).
- Query extensions:
ECoreNetto.Extensions/{Package,Class,StructuralFeature,ModelElement}Extensions.cs.
Two hard facts that shape the design
- EcoreNetto model classes are NOT pure POCOs. Each embeds hand-written parsing (
internal override void SetProperties(), protected override void DeserializeChildNode(XmlNode)), ctors taking (Resource.Resource, ILoggerFactory?), typed loggers, and derived accessors (e.g. EClass.AllEStructuralFeaturesOrderByName). None of that is derivable from ecore.ecore. → The generator must own only the data surface; behavior stays hand-written via partial classes.
ecore.ecore carries ZERO documentation annotations (only suppressedIsSetVisibility/suppressedUnsetVisibility GenModel details), so generated <summary> would be placeholders — violating the CLAUDE.md rule that every member has a real XML-doc.
Decisions (agreed)
- Scope: Full self-hosting — build the generator AND flip the ~20 shipping
ModelElement/** classes to partial, moving their data surface into generated AutoGen/ files, proving regenerate → build → still parses.
- Docs: side-car JSON map — a
ecore-documentation.json seeded from the existing hand-written XML-doc, looked up during generation (fallback to model annotations). Do not mutate ecore.ecore (canonical shared EMF test data).
Plan
New projects (add to EcoreNetto.sln)
ECoreNetto.CodeGenerator — net10.0, <IsPackable>false</IsPackable> (tooling; Roslyn needs net10). ProjectRefs: ECoreNetto, ECoreNetto.Extensions, ECoreNetto.HandleBars. PackageRefs: Microsoft.CodeAnalysis.CSharp, HandlebarsDotNet, Handlebars.Net.Helpers, Microsoft.Extensions.Logging.Abstractions. Templates/*.hbs + Resources/ecore-documentation.json copied to output.
ECoreNetto.CodeGenerator.Tests — net10.0, NUnit 4 + Moq + coverlet. Link TestData/ecore.ecore into output Data/. Committed goldens under Expected/AutoGen*/.
Generator hierarchy (namespace ECoreNetto.CodeGenerator.Generators)
Port from uml4net.CodeGenerator/Generators/, but the domain type is EPackage (not XmiReaderResult):
Generator.cs (abstract) — copy verbatim: CodeCleanup (Roslyn AdhocWorkspace + Formatter.Format), WriteAsync UTF-8.
HandleBarsGenerator.cs (abstract) — shared Handlebars env, RegisterTemplate(name) compiling Templates/{name}.hbs.
EcoreHandleBarsGenerator.cs (abstract) — GenerateAsync(EPackage rootPackage, DirectoryInfo outputDirectory) + Flatten(EPackage) reusing PackageExtensions.QueryPackages.
CorePocoGenerator.cs — fans to GenerateEnumerationsAsync/GenerateInterfacesAsync/GenerateClassesAsync; per-name methods so goldens assert one file at a time. Do NOT abstract-filter (unlike uml4net) — EcoreNetto ships abstract classes; emit public abstract partial class when EClass.Abstract, else public partial class.
Transformers/ModelTransformer.cs — port the C#-illegal-name fixer (likely a no-op for ecore.ecore; include for parity + future models).
Helpers & extensions
Reuse as-is: StringHelper, StructuralFeatureHelper, GeneralizationHelper (the key reuse), BooleanHelper (register it — currently unregistered); extensions QueryPackages, QueryTypeHierarchy, QuerySpecializations, QueryTypeName, QueryIsNullable, QueryIsEnumerable, QueryIsContainment, QueryHasDefaultValue, QueryRawDocumentation.
New in ECoreNetto.Extensions:
StructuralFeatureExtensions.QueryCSharpTypeName(this EStructuralFeature) — datatype→alias + multiplicity/containment wrapping: scalar → alias or class name (? when nullable); enumerable + containment → ContainerList<T>; enumerable + non-containment → List<T> (reproduces EClass.ESuperTypes = List<EClass> vs EClass.EOperations = ContainerList<EOperation>).
- Static datatype→alias map:
EString→string, EBoolean→bool, EInt→int, EIntegerObject→int?, ELong→long, EDouble→double, EFloat→float, EChar→char, EByte→byte, EByteArray→byte[], EBigDecimal→decimal, EBigInteger→System.Numerics.BigInteger, EDate→System.DateTime, EJavaObject/EJavaClass→object. Unmapped → the datatype's Name.
New in ECoreNetto.HandleBars:
PropertyHelper (Property.WriteForClass-style) — emits the full property line (modifiers, type via QueryCSharpTypeName, name via CapitalizeFirstLetter, { get; internal set; }/{ get; private set; } vs collection without setter, defaultValueLiteral initializer: bool lower-cased, string quoted, enum Type.Literal).
DocumentationProvider + a Documentation helper: look up ecore-documentation.json first, fall back to QueryRawDocumentation.
Templates (ECoreNetto.CodeGenerator/Templates/)
Thin — header + "AUTOGENERATED FILE" banner + fixed usings (inside namespace per repo style) + [GeneratedCode("ECoreNetto","latest")]:
ecore-to-poco-interface.hbs → I{Class}.cs
ecore-to-poco-class.hbs → {Class}.cs (emits partial)
ecore-to-enumeration.hbs → {Enum}.cs
Production flip (per file in ECoreNetto/ModelElement/**, ~20 classes — all currently non-partial)
- Add
partial to the declaration.
- Move the generatable data surface (public auto-properties + XML-doc:
Abstract, Interface, ESuperTypes, EOperations, EStructuralFeatures, …) into the generated partial ECoreNetto/AutoGen/{Name}.cs.
- Keep in the behavior file: ctors, logger fields,
const strings, SetProperties(), DeserializeChildNode(XmlNode), BuildIdentifier(), derived accessors (EOperationsOrderByName, AllEStructuralFeatures, AllEStructuralFeaturesOrderByName, InterfaceAndOwnStructuralFeatures).
Collections: declare in the generated partial, construct in the hand-written ctor (legal across partials). ContainerList<T> and base infra stay hand-written — the generator emits ContainerList<T>/List<T> but never generates ContainerList itself.
Driving generation (NUnit, not CLI)
ECoreNetto.CodeGenerator.Tests/Generators/CorePocoGeneratorTestFixture.cs (mirror uml4net's):
[SetUp] loads Data/ecore.ecore via ResourceSet/Resource.Load, runs ModelTransformer.
[Test] + TestCaseSource over class/enum names → generate one file to scratch, Assert.That(code, Is.EqualTo(expected)) vs Expected/AutoGen*/{Name}.cs.
- Guard/throws tests for the 80% coverage gate.
[Explicit("Regenerates the production ECoreNetto model-element data-surface partials")] writing into ECoreNetto/AutoGen/. Invoke: dotnet test --filter Name~Regenerate_Model_Element_Classes.
CLI (ECoreNetto.Tools) deferred — a generate-model command doesn't fit ReportHandler (single --output-report FileInfo; model gen needs an output directory + multi-file). The [Explicit] test is the source of truth.
Mapping rules (ecore → C#)
| ecore construct |
C# emission |
EClass (non-abstract) |
public partial class {Name} + public partial interface I{Name} |
EClass abstract="true" |
public abstract partial class {Name} (do not skip) |
eSuperTypes (0) |
class : I{Self}; interface: no base |
eSuperTypes (≥1) |
class : {firstSuper}, I{Self}; interface : I{super1}, I{super2}… |
upperBound == 1/unset |
scalar property |
lowerBound == 0 + scalar |
nullable T? |
| enumerable + containment |
ContainerList<T> |
| enumerable + non-containment |
List<T> |
EReference.eOpposite |
emit property only; opposite-wiring stays hand-written |
EAttribute.eType = EEnum |
property typed as the enum |
EEnum |
public enum {Name} with EEnumLiteral.Name = Value |
EDataType (EString/EInt/…) |
not a file; resolved to C# alias by the map |
defaultValueLiteral |
initializer = {literal}; |
changeable="false" |
{ get; } (no setter) |
Verification
- Goldens:
dotnet test ECoreNetto.CodeGenerator.Tests/ECoreNetto.CodeGenerator.Tests.csproj — each generated file byte-compared to Expected/AutoGen*/{Name}.cs, seeded from today's hand-written surface.
- Coverage gate:
... --collect:"XPlat Code Coverage" --settings coverlet.runsettings — new lines ≥ 80%.
- Explicit regeneration:
... --filter Name~Regenerate_Model_Element_Classes writes ECoreNetto/AutoGen/; git diff --stat ECoreNetto/AutoGen/ must be empty on a second run (idempotence).
- Round-trip (the real proof, post-flip):
dotnet build EcoreNetto.sln → dotnet test ECoreNetto.Tests/ECoreNetto.Tests.csproj — existing loader/parse fixtures still deserialize ecore.ecore/recipe.ecore. Add an assertion that reloads ecore.ecore and confirms EClass.EStructuralFeatures/ESuperTypes populate.
Suggested phasing
- Generator + helpers/extensions + templates + goldens — zero production edits; goldens prove byte-for-byte reproduction of today's surface.
- Production flip — add
partial to the ~20 classes, move data surface to ECoreNetto/AutoGen/, wire the [Explicit] regen test, verify round-trip.
- (Optional, later)
generate-model CLI convenience command.
Risks & open decisions
- Phase 2 flip is the risk (~20 shipping classes). Land Phase 1 goldens first; flip only when byte-equal to today's surface.
- Golden seeding / current inconsistency: if the 20 classes differ in
internal set vs private set or doc formatting, either normalize the hand-written code first or teach the template exact per-member modifiers.
- Multiple inheritance:
Generalization.Classes takes ESuperTypes[0] as the C# base — spot-check each class's first super in ecore.ecore matches the intended hand-written base.
EGenericType/ETypeParameter/EGenericSuperTypes: exist as hand-written classes; decide whether the first cut generates them or leaves them fully hand-written.
Summary
Make EcoreNetto self-hosting: read
TestData/ecore.ecore(the self-describing Ecore meta-metamodel) with EcoreNetto's own reader and run Handlebars templates to regenerate EcoreNetto's ownModelElement/**classes — mirroring how the sibling projectuml4netregenerates itself fromUML.xmi.This issue captures a researched implementation plan to come back to later. Not yet scheduled.
Background — how uml4net does it (reference design)
uml4net.CodeGenerator(not shipped,IsPackable=false) readsUML.xmiinto an in-memory graph, then Handlebars templates emit the C# POCO interfaces/classes/enums (and separate XMI readers/writers) that make up uml4net itself — a closed self-generating loop.Generator(RoslynFormatter.Formatcleanup + UTF-8 write) →HandleBarsGenerator(shared Handlebars env,RegisterHelpers/RegisterTemplates) →UmlHandleBarsGenerator→CorePocoGenerator/XmiReaderGenerator/XmiWriterGenerator. AModelTransformerruns first to fix C#-illegal names.Expected/AutoGen*/; a separate[Explicit("Regenerates ... production code")]test writes directly into the real source dirs.What EcoreNetto already has
ReportGenerator.LoadRootPackage(FileInfo)→ResourceSet.CreateResource→Resource.Load→ECoreParser.ParseXmlproduces theEPackageobject graph.ecore.ecoreis already test data.ECoreNetto.Reporting/Generators/HandleBarsReportGenerator.cs(+CreateHandlebarsPayloadtree-flatten).ECoreNetto.HandleBars/GeneralizationHelper.cs(Generalization.Classes→: FirstSuper, ISelf,Generalization.Interfaces→: IFoo, IBar), plusStructuralFeatureHelper,StringHelper,BooleanHelper(defined but unregistered).ECoreNetto.Extensions/{Package,Class,StructuralFeature,ModelElement}Extensions.cs.Two hard facts that shape the design
internal override void SetProperties(),protected override void DeserializeChildNode(XmlNode)), ctors taking(Resource.Resource, ILoggerFactory?), typed loggers, and derived accessors (e.g.EClass.AllEStructuralFeaturesOrderByName). None of that is derivable fromecore.ecore. → The generator must own only the data surface; behavior stays hand-written viapartialclasses.ecore.ecorecarries ZERO documentation annotations (onlysuppressedIsSetVisibility/suppressedUnsetVisibilityGenModel details), so generated<summary>would be placeholders — violating the CLAUDE.md rule that every member has a real XML-doc.Decisions (agreed)
ModelElement/**classes topartial, moving their data surface into generatedAutoGen/files, proving regenerate → build → still parses.ecore-documentation.jsonseeded from the existing hand-written XML-doc, looked up during generation (fallback to model annotations). Do not mutateecore.ecore(canonical shared EMF test data).Plan
New projects (add to
EcoreNetto.sln)ECoreNetto.CodeGenerator—net10.0,<IsPackable>false</IsPackable>(tooling; Roslyn needs net10). ProjectRefs:ECoreNetto,ECoreNetto.Extensions,ECoreNetto.HandleBars. PackageRefs:Microsoft.CodeAnalysis.CSharp,HandlebarsDotNet,Handlebars.Net.Helpers,Microsoft.Extensions.Logging.Abstractions.Templates/*.hbs+Resources/ecore-documentation.jsoncopied to output.ECoreNetto.CodeGenerator.Tests—net10.0, NUnit 4 + Moq + coverlet. LinkTestData/ecore.ecoreinto outputData/. Committed goldens underExpected/AutoGen*/.Generator hierarchy (
namespace ECoreNetto.CodeGenerator.Generators)Port from
uml4net.CodeGenerator/Generators/, but the domain type isEPackage(notXmiReaderResult):Generator.cs(abstract) — copy verbatim:CodeCleanup(RoslynAdhocWorkspace+Formatter.Format),WriteAsyncUTF-8.HandleBarsGenerator.cs(abstract) — shared Handlebars env,RegisterTemplate(name)compilingTemplates/{name}.hbs.EcoreHandleBarsGenerator.cs(abstract) —GenerateAsync(EPackage rootPackage, DirectoryInfo outputDirectory)+Flatten(EPackage)reusingPackageExtensions.QueryPackages.CorePocoGenerator.cs— fans toGenerateEnumerationsAsync/GenerateInterfacesAsync/GenerateClassesAsync; per-name methods so goldens assert one file at a time. Do NOT abstract-filter (unlike uml4net) — EcoreNetto ships abstract classes; emitpublic abstract partial classwhenEClass.Abstract, elsepublic partial class.Transformers/ModelTransformer.cs— port the C#-illegal-name fixer (likely a no-op forecore.ecore; include for parity + future models).Helpers & extensions
Reuse as-is:
StringHelper,StructuralFeatureHelper,GeneralizationHelper(the key reuse),BooleanHelper(register it — currently unregistered); extensionsQueryPackages,QueryTypeHierarchy,QuerySpecializations,QueryTypeName,QueryIsNullable,QueryIsEnumerable,QueryIsContainment,QueryHasDefaultValue,QueryRawDocumentation.New in
ECoreNetto.Extensions:StructuralFeatureExtensions.QueryCSharpTypeName(this EStructuralFeature)— datatype→alias + multiplicity/containment wrapping: scalar → alias or class name (?when nullable); enumerable + containment →ContainerList<T>; enumerable + non-containment →List<T>(reproducesEClass.ESuperTypes=List<EClass>vsEClass.EOperations=ContainerList<EOperation>).EString→string,EBoolean→bool,EInt→int,EIntegerObject→int?,ELong→long,EDouble→double,EFloat→float,EChar→char,EByte→byte,EByteArray→byte[],EBigDecimal→decimal,EBigInteger→System.Numerics.BigInteger,EDate→System.DateTime,EJavaObject/EJavaClass→object. Unmapped → the datatype'sName.New in
ECoreNetto.HandleBars:PropertyHelper(Property.WriteForClass-style) — emits the full property line (modifiers, type viaQueryCSharpTypeName, name viaCapitalizeFirstLetter,{ get; internal set; }/{ get; private set; }vs collection without setter,defaultValueLiteralinitializer: bool lower-cased, string quoted, enumType.Literal).DocumentationProvider+ aDocumentationhelper: look upecore-documentation.jsonfirst, fall back toQueryRawDocumentation.Templates (
ECoreNetto.CodeGenerator/Templates/)Thin — header + "AUTOGENERATED FILE" banner + fixed usings (inside namespace per repo style) +
[GeneratedCode("ECoreNetto","latest")]:ecore-to-poco-interface.hbs→I{Class}.csecore-to-poco-class.hbs→{Class}.cs(emitspartial)ecore-to-enumeration.hbs→{Enum}.csProduction flip (per file in
ECoreNetto/ModelElement/**, ~20 classes — all currently non-partial)partialto the declaration.Abstract,Interface,ESuperTypes,EOperations,EStructuralFeatures, …) into the generated partialECoreNetto/AutoGen/{Name}.cs.conststrings,SetProperties(),DeserializeChildNode(XmlNode),BuildIdentifier(), derived accessors (EOperationsOrderByName,AllEStructuralFeatures,AllEStructuralFeaturesOrderByName,InterfaceAndOwnStructuralFeatures).Collections: declare in the generated partial, construct in the hand-written ctor (legal across partials).
ContainerList<T>and base infra stay hand-written — the generator emitsContainerList<T>/List<T>but never generatesContainerListitself.Driving generation (NUnit, not CLI)
ECoreNetto.CodeGenerator.Tests/Generators/CorePocoGeneratorTestFixture.cs(mirror uml4net's):[SetUp]loadsData/ecore.ecoreviaResourceSet/Resource.Load, runsModelTransformer.[Test]+TestCaseSourceover class/enum names → generate one file to scratch,Assert.That(code, Is.EqualTo(expected))vsExpected/AutoGen*/{Name}.cs.[Explicit("Regenerates the production ECoreNetto model-element data-surface partials")]writing intoECoreNetto/AutoGen/. Invoke:dotnet test --filter Name~Regenerate_Model_Element_Classes.CLI (
ECoreNetto.Tools) deferred — agenerate-modelcommand doesn't fitReportHandler(single--output-reportFileInfo; model gen needs an output directory + multi-file). The[Explicit]test is the source of truth.Mapping rules (ecore → C#)
EClass(non-abstract)public partial class {Name}+public partial interface I{Name}EClass abstract="true"public abstract partial class {Name}(do not skip)eSuperTypes(0): I{Self}; interface: no baseeSuperTypes(≥1): {firstSuper}, I{Self}; interface: I{super1}, I{super2}…upperBound == 1/unsetlowerBound == 0+ scalarT?ContainerList<T>List<T>EReference.eOppositeEAttribute.eType = EEnumEEnumpublic enum {Name}withEEnumLiteral.Name = ValueEDataType(EString/EInt/…)defaultValueLiteral= {literal};changeable="false"{ get; }(no setter)Verification
dotnet test ECoreNetto.CodeGenerator.Tests/ECoreNetto.CodeGenerator.Tests.csproj— each generated file byte-compared toExpected/AutoGen*/{Name}.cs, seeded from today's hand-written surface.... --collect:"XPlat Code Coverage" --settings coverlet.runsettings— new lines ≥ 80%.... --filter Name~Regenerate_Model_Element_ClasseswritesECoreNetto/AutoGen/;git diff --stat ECoreNetto/AutoGen/must be empty on a second run (idempotence).dotnet build EcoreNetto.sln→dotnet test ECoreNetto.Tests/ECoreNetto.Tests.csproj— existing loader/parse fixtures still deserializeecore.ecore/recipe.ecore. Add an assertion that reloadsecore.ecoreand confirmsEClass.EStructuralFeatures/ESuperTypespopulate.Suggested phasing
partialto the ~20 classes, move data surface toECoreNetto/AutoGen/, wire the[Explicit]regen test, verify round-trip.generate-modelCLI convenience command.Risks & open decisions
internal setvsprivate setor doc formatting, either normalize the hand-written code first or teach the template exact per-member modifiers.Generalization.ClassestakesESuperTypes[0]as the C# base — spot-check each class's first super inecore.ecorematches the intended hand-written base.EGenericType/ETypeParameter/EGenericSuperTypes: exist as hand-written classes; decide whether the first cut generates them or leaves them fully hand-written.