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

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
The four-layer MOF hierarchy as the CMF uses it: M3 is written once (MetaConcept, MetaProperty…), M2 hosts the DSL attribute families, M1 is the application code and M0 the runtime instances that code produces.
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.


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
The reflexive fixed point of M3: MetaConcept carries MetaProperty, MetaReference, MetaConstraint and MetaInherits, and describes itself via the same machinery — there is no M4.

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.

⬇ Download