Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part III: Meta-Meta-model Foundation (M3)

The Problem M3 Solves

The CMF has six DSLs. Each DSL defines attributes ([AggregateRoot], [ContentPart], [Workflow], etc.). Each attribute has properties. Each property has constraints. The source generators must understand all of this.

Without a meta-metamodel, each DSL is an island. The DDD generator knows about [AggregateRoot] but not [ContentPart]. The Content generator knows about [StreamField] but not [Workflow]. Cross-DSL validation (e.g., "a workflow can only be attached to an aggregate") requires hard-coded knowledge of every DSL combination.

The M3 meta-metamodel solves this by providing five primitives that all DSLs are built from. A DSL is no longer opaque -- it is a structured collection of MetaConcepts with MetaProperties, MetaReferences, and MetaConstraints. Any tool that understands M3 can inspect any DSL.


The Four-Layer Hierarchy

The CMF uses the four-layer architecture from the OMG Meta-Object Facility (MOF):

Diagram
Layer What it is Who writes it Example
M3 Primitives for building DSLs Framework authors (once) MetaConcept, MetaProperty
M2 DSL attributes DSL authors [AggregateRoot], [ContentPart]
M1 Domain models using the DSLs Application developers class Order, class BlogPost
M0 Runtime instances The program at runtime order.Total = 1500

Key insight: M3 is written once and never changes. M2 DSLs are written per-concern (DDD, Content, Admin, etc.). M1 models are written per-application. M0 is runtime data. Each layer defines the structure of the layer below it.


The Five M3 Primitives

1. MetaConcept

A MetaConcept declares that an attribute represents a modeling primitive. It is the "thing" in the modeling language.

namespace Cmf.Meta.Lib;

/// <summary>
/// Declares that an attribute class represents a modeling concept.
/// All DSL attributes must be annotated with [MetaConcept].
/// This is the entry point for the metamodel registry.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
[MetaConcept("MetaConcept")] // Self-describing: MetaConcept IS a MetaConcept
public sealed class MetaConceptAttribute : Attribute
{
    public string Name { get; }
    public string? Description { get; set; }

    public MetaConceptAttribute(string name) => Name = name;
}

Usage at M2:

// DDD DSL
[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]
public sealed class AggregateRootAttribute : Attribute { }

// Content DSL
[MetaConcept("ContentPart")]
public sealed class ContentPartAttribute : Attribute { }

// Workflow DSL
[MetaConcept("Workflow")]
public sealed class WorkflowAttribute : Attribute { }

Every [MetaConcept] attribute is discovered by the Stage 0 generator and registered in the metamodel registry. This means any DSL is automatically known to the system.

2. MetaProperty

A MetaProperty declares a typed configuration slot on a concept.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
[MetaConcept("MetaProperty")]
public sealed class MetaPropertyAttribute : Attribute
{
    public string Name { get; }
    public string Type { get; }
    public bool Required { get; set; } = false;
    public object? DefaultValue { get; set; }

    public MetaPropertyAttribute(string name, string type)
    {
        Name = name;
        Type = type;
    }
}

Usage at M2:

[MetaConcept("AggregateRoot")]
public sealed class AggregateRootAttribute : Attribute
{
    [MetaProperty("Name", "string", Required = true)]
    public string Name { get; set; }

    [MetaProperty("BoundedContext", "string")]
    public string? BoundedContext { get; set; }
}

The Stage 0 generator reads MetaProperties to know what configuration each concept accepts. Stage 1 validates that required properties are set.

3. MetaReference

A MetaReference declares a directed association between concepts.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
[MetaConcept("MetaReference")]
public sealed class MetaReferenceAttribute : Attribute
{
    public string Name { get; }
    public string TargetConcept { get; }
    public string Multiplicity { get; set; } = "0..*"; // "1", "0..1", "1..*", "0..*"
    public bool IsContainment { get; set; } = false;

    public MetaReferenceAttribute(string name, string targetConcept)
    {
        Name = name;
        TargetConcept = targetConcept;
    }
}

Usage at M2:

[MetaConcept("Composition")]
public sealed class CompositionAttribute : Attribute
{
    [MetaReference("Target", "Entity", Multiplicity = "1", IsContainment = true)]
    public Type TargetType { get; set; }
}

MetaReferences tell the generator about relationships between concepts. IsContainment = true means the parent owns the child (relevant for cascade delete and aggregate boundaries).

4. MetaConstraint

A MetaConstraint declares a validation rule that must hold for a concept to be valid.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[MetaConcept("MetaConstraint")]
public sealed class MetaConstraintAttribute : Attribute
{
    public string Name { get; }
    public string Expression { get; } // OCL-like expression
    public string Message { get; set; } = "";
    public DiagnosticSeverity Severity { get; set; } = DiagnosticSeverity.Error;

    public MetaConstraintAttribute(string name, string expression)
    {
        Name = name;
        Expression = expression;
    }
}

Usage at M2:

[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]
[MetaConstraint(
    "MustHaveId",
    "Properties.Any(p => p.IsAnnotatedWith('EntityId'))",
    Message = "Aggregate root must have a property with [EntityId]",
    Severity = DiagnosticSeverity.Error)]
[MetaConstraint(
    "MustHaveInvariant",
    "Methods.Any(m => m.IsAnnotatedWith('Invariant'))",
    Message = "Aggregate root should have at least one [Invariant] method",
    Severity = DiagnosticSeverity.Warning)]
public sealed class AggregateRootAttribute : Attribute { }

MetaConstraints are evaluated at Stage 1. If a constraint fails, the generator emits a compiler diagnostic. This means invalid models do not compile.

5. MetaInherits

MetaInherits declares metamodel-level inheritance between concepts.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
[MetaConcept("MetaInherits")]
public sealed class MetaInheritsAttribute : Attribute
{
    public string ParentConcept { get; }
    public MetaInheritsAttribute(string parentConcept) => ParentConcept = parentConcept;
}

Usage at M2:

[MetaConcept("AggregateRoot")]
[MetaInherits("Entity")]  // AggregateRoot IS-A Entity at the metamodel level
public sealed class AggregateRootAttribute : Attribute { }

[MetaConcept("Entity")]
[MetaConstraint("MustBeComposed",
    "IsReachableViaCompositionFrom('AggregateRoot')",
    Message = "Entity must be reachable via [Composition] from an aggregate root")]
public sealed class EntityAttribute : Attribute { }

MetaInherits allows AggregateRoot to inherit all MetaProperties and MetaConstraints from Entity. Generators treating Entity also treat AggregateRoot.


The Self-Describing Fixed Point

Notice that MetaConceptAttribute is annotated with [MetaConcept("MetaConcept")]:

[MetaConcept("MetaConcept")] // ← I am a MetaConcept that describes MetaConcepts
public sealed class MetaConceptAttribute : Attribute { }

This is the fixed point of the metamodeling hierarchy. M3 describes itself. There is no M4. The five primitives are sufficient to describe:

  1. Themselves (M3)
  2. Any DSL built on them (M2)
  3. Any model using those DSLs (M1)

This is not circular -- it is reflexive. MetaConcept is the smallest vocabulary needed to describe modeling languages. Everything else is built from these five axioms.

Diagram

Stage 0: Metamodel Registry Generation

The Stage 0 source generator runs first and produces a registry of all M2 concepts:

// Generated: MetamodelRegistry.g.cs
public static class MetamodelRegistry
{
    public static IReadOnlyDictionary<string, ConceptDescriptor> Concepts { get; } =
        new Dictionary<string, ConceptDescriptor>
        {
            ["AggregateRoot"] = new ConceptDescriptor(
                Name: "AggregateRoot",
                AttributeType: typeof(AggregateRootAttribute),
                Inherits: new[] { "Entity" },
                Properties: new[]
                {
                    new PropertyDescriptor("Name", "string", Required: true),
                    new PropertyDescriptor("BoundedContext", "string", Required: false)
                },
                Constraints: new[]
                {
                    new ConstraintDescriptor("MustHaveId",
                        "Properties.Any(p => p.IsAnnotatedWith('EntityId'))",
                        DiagnosticSeverity.Error),
                    new ConstraintDescriptor("MustHaveInvariant",
                        "Methods.Any(m => m.IsAnnotatedWith('Invariant'))",
                        DiagnosticSeverity.Warning)
                }),

            ["Entity"] = new ConceptDescriptor(
                Name: "Entity",
                AttributeType: typeof(EntityAttribute),
                Inherits: Array.Empty<string>(),
                Properties: new[]
                {
                    new PropertyDescriptor("Name", "string", Required: true)
                },
                Constraints: new[]
                {
                    new ConstraintDescriptor("MustBeComposed",
                        "IsReachableViaCompositionFrom('AggregateRoot')",
                        DiagnosticSeverity.Error)
                }),

            ["ContentPart"] = new ConceptDescriptor(
                Name: "ContentPart",
                AttributeType: typeof(ContentPartAttribute),
                Inherits: Array.Empty<string>(),
                Properties: new[]
                {
                    new PropertyDescriptor("Name", "string", Required: true)
                },
                Constraints: Array.Empty<ConstraintDescriptor>()),

            // ... all other concepts from all DSLs
        };
}

public record ConceptDescriptor(
    string Name,
    Type AttributeType,
    string[] Inherits,
    PropertyDescriptor[] Properties,
    ConstraintDescriptor[] Constraints);

public record PropertyDescriptor(string Name, string Type, bool Required);
public record ConstraintDescriptor(string Name, string Expression, DiagnosticSeverity Severity);

This registry is the single source of truth for all DSLs. Stage 1 uses it to validate M1 models. Stages 2-4 use it to generate code. The CLI uses it to scaffold. The admin DSL uses it to auto-generate forms.


How M2 DSLs Emerge from M3

Each DSL is simply a collection of M3-annotated attribute classes. Adding a new DSL means:

  1. Create a new Cmf.NewDsl.Lib project
  2. Define attribute classes annotated with [MetaConcept], [MetaProperty], [MetaConstraint]
  3. The Stage 0 generator automatically discovers and registers them
  4. The Stage 1 validator automatically validates models using them
  5. A Stage 2+ generator produces domain-specific code

No modification to the M3 layer is needed. No modification to Stage 0 or Stage 1. The DSL is plug-and-play because it speaks the M3 vocabulary.

This is how the Requirements DSL was added as the sixth DSL without modifying the other five. And this is how a seventh, eighth, or ninth DSL could be added in the future (Permissions DSL? Search DSL? Notification DSL?).


Comparison with Other Metamodeling Approaches

Approach Level Expression Validation Code Generation
OMG MOF / EMF M3 Ecore models (XML/XMI) OCL constraints Template-based (Acceleo, Xtext)
TypeScript Decorators M2 Runtime decorators Runtime checks None (interpreted)
Roslyn Analyzers M2 C# attributes Ad-hoc DiagnosticAnalyzer Ad-hoc ISourceGenerator
This CMF (M3) M3 C# attributes on attributes MetaConstraint → Roslyn diagnostics Multi-stage pipeline

The key difference: this CMF uses C# attributes as both the DSL surface (M2) and the metamodel surface (M3). There is no separate modeling language (no XML, no JSON, no YAML). Everything is C#. The compiler is the modeling tool.


Summary

Primitive Role Stage
MetaConcept Declares a modeling concept Stage 0: registered
MetaProperty Declares a typed configuration slot Stage 0: registered, Stage 1: validated
MetaReference Declares a directed association Stage 0: registered, Stage 1: validated
MetaConstraint Declares a validation rule (OCL-like) Stage 0: registered, Stage 1: evaluated
MetaInherits Declares metamodel inheritance Stage 0: inheritance chain resolved

Five primitives. Self-describing. No M4. Every DSL in the CMF is built from these five axioms. The Stage 0 generator reads them. The Stage 1 validator enforces them. Stages 2-4 use the validated model graph to generate the entire application stack.