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):
| 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;
}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 { }// 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;
}
}[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; }
}[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;
}
}[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; }
}[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;
}
}[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 { }[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;
}[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 { }[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 { }[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:
- Themselves (M3)
- Any DSL built on them (M2)
- 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.
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);// 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:
- Create a new
Cmf.NewDsl.Libproject - Define attribute classes annotated with
[MetaConcept],[MetaProperty],[MetaConstraint] - The Stage 0 generator automatically discovers and registers them
- The Stage 1 validator automatically validates models using them
- 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.