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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions