Skip to content

Self-generation: regenerate EcoreNetto's own model classes from ecore.ecore (uml4net-style) #103

Description

@samatstariongroup

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) → UmlHandleBarsGeneratorCorePocoGenerator/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.CreateResourceResource.LoadECoreParser.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

  1. 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.
  2. 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.CodeGeneratornet10.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.Testsnet10.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.hbsI{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)

  1. Add partial to the declaration.
  2. Move the generatable data surface (public auto-properties + XML-doc: Abstract, Interface, ESuperTypes, EOperations, EStructuralFeatures, …) into the generated partial ECoreNetto/AutoGen/{Name}.cs.
  3. 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

  1. 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.
  2. Coverage gate: ... --collect:"XPlat Code Coverage" --settings coverlet.runsettings — new lines ≥ 80%.
  3. Explicit regeneration: ... --filter Name~Regenerate_Model_Element_Classes writes ECoreNetto/AutoGen/; git diff --stat ECoreNetto/AutoGen/ must be empty on a second run (idempotence).
  4. Round-trip (the real proof, post-flip): dotnet build EcoreNetto.slndotnet 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

  1. Generator + helpers/extensions + templates + goldens — zero production edits; goldens prove byte-for-byte reproduction of today's surface.
  2. Production flip — add partial to the ~20 classes, move data surface to ECoreNetto/AutoGen/, wire the [Explicit] regen test, verify round-trip.
  3. (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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions