Part I: Vision
What Is a CMF?
A CMS (Content Management System) is an application. You install it, configure it, extend it with plugins. WordPress, Drupal, Strapi -- these are applications that manage content. A developer adapts the application to the domain.
A CMF (Content Management Framework) is a framework in the developer sense. It provides primitives, DSLs, and code generation to build domain-specific applications from scratch. The developer defines the domain; the framework generates the application. There is no preinstalled blog, no default theme, no comments module. The CMF starts empty.
The distinction matters because it determines who is in control:
| Aspect | CMS | CMF |
|---|---|---|
| Starting point | Prebuilt application | Empty project |
| Extension model | Plugins, hooks, filters | DSL attributes, source generation |
| Data model | Generic content types | Domain-specific aggregates |
| Type safety | String-based (post meta, custom fields) | Compiler-checked (typed entities) |
| Generated code | None (interpreted at runtime) | Full stack (entities → API → UI) |
| Performance | Runtime interpretation | Ahead-of-time compilation |
| Who controls architecture | The CMS | The developer |
The Diem Project: Where This Started
In 2010, the Diem Project was a Content Management Framework built on Symfony 1.4 and Doctrine ORM. It was not WordPress. It was not Drupal. It started empty.
A developer defined a data schema in schema.yml, declared modules in modules.yml, and Diem generated code that was 100% specific to the site's needs:
- Backend: Admin interfaces generated from the schema (list views, forms, filters)
- Frontend: Pages composed from Layouts, Zones, and Widgets
- Data: Doctrine ORM entities generated from YAML
The key insight was: define the domain once, generate everything else. The developer wrote ~200 lines of YAML. Diem generated ~5,000 lines of PHP. The generated code was not version-controlled -- it was regenerated on each dm:generate run. But it was extensible through a three-layer class hierarchy:
YourModule-- your code, created once as a stub if it doesn't exist, never overwrittenYourGeneratedModule-- regenerated every time from the YAML schemaModule-- Diem's base class
This meant you could override any generated behavior in YourModule without fear of losing changes. The generator would only touch YourGeneratedModule, and your customizations in YourModule survived every regeneration cycle.
What Diem lacked:
- Type safety: YAML is stringly-typed. A typo in
schema.ymlwas a runtime error, not a compile error - DDD concepts: No aggregates, no bounded contexts, no commands/events, no sagas
- Content composition: No content parts (Orchard), no StreamFields (Wagtail), no structured blocks
- Workflow: No editorial pipelines, no approval gates, no translations
- Requirements tracking: No connection between "why we build it" and "what we built"
- Testing integration: No way to link tests to features
This document describes the next generation: a CMF that replaces YAML with C# attributes, runtime interpretation with Roslyn source generation, and adds DDD, content composition, workflows, and a type-safe requirements chain.
The Transformation
| Diem (PHP / Symfony 1.4, 2010) | CMF (C# / .NET 10, 2026) |
|---|---|
schema.yml → Doctrine ORM |
[AggregateRoot], [Entity], [ValueObject] → Roslyn → EF Core |
modules.yml → admin + front modules |
[AdminModule] + [PageWidget] → Roslyn → Blazor components |
| Page → Layout → Zone → Widget (server-rendered) | Page → Layout → Zone → Widget (Blazor WASM SPA + API) |
| Doctrine model classes | Shared C# kernel (compiled to both server + WASM) |
dm:setup CLI |
cmf new, cmf add aggregate, cmf design (interactive TUI) |
Admin generator (generator.yml) |
Source-generated Blazor admin CRUD |
| Runtime PHP interpretation | Ahead-of-time Roslyn compilation |
What Diem did not have -- and what this CMF adds by learning from the broader landscape:
| Origin | Pattern | CMF Realization |
|---|---|---|
| Orchard Core (.NET) | Content Parts -- composable cross-cutting concerns | [ContentPart], [HasPart] |
| Wagtail (Django) | StreamField -- composable typed content blocks as JSON | [ContentBlock], [StreamField] |
| Drupal | Content versioning, draft/publish, taxonomy | [Versionable], [Taxonomy] |
| Symfony CMF | Dynamic routing from content tree | Page tree → URL resolution |
| Strapi | Auto REST + GraphQL API from schema | Generated API controllers + GraphQL schema |
| Evans (DDD) | Aggregates, bounded contexts, ubiquitous language | [AggregateRoot], [BoundedContext], [Composition] |
| Specification Pattern | Behavioral contracts for domain rules | IUserRolesSpec interfaces with [ForRequirement] |
Why Four Disciplines Together?
This CMF combines four disciplines that are usually practiced separately:
1. Requirements as Code
Requirements are not Jira tickets. They are C# types. Acceptance criteria are abstract methods. The compiler enforces that every requirement has a specification, every specification has an implementation, and every implementation has tests.
// A requirement IS a type
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
// Each acceptance criterion IS an abstract method
public abstract AcceptanceCriterionResult AdminCanAssignRoles(
UserId actingUser, UserId targetUser, RoleId role);
}// A requirement IS a type
public abstract record UserRolesFeature : Feature<PlatformScalabilityEpic>
{
// Each acceptance criterion IS an abstract method
public abstract AcceptanceCriterionResult AdminCanAssignRoles(
UserId actingUser, UserId targetUser, RoleId role);
}This eliminates the gap between "what we decided to build" and "what we actually built." The chain is compiler-enforced, IDE-navigable, and CI-validated.
2. Test-Driven Development
Tests are not an afterthought. They are linked to requirements by type reference. The [Verifies] attribute uses typeof() and nameof() -- compiler-checked. Adding a new acceptance criterion to a Feature produces a compiler warning until a test exists for it.
[Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
public void Admin_can_assign_role()
{
var result = _authService.AssignRole(admin, target, editorRole);
Assert.That(result.IsSuccess, Is.True);
}[Verifies(typeof(UserRolesFeature), nameof(UserRolesFeature.AdminCanAssignRoles))]
public void Admin_can_assign_role()
{
var result = _authService.AssignRole(admin, target, editorRole);
Assert.That(result.IsSuccess, Is.True);
}Quality gates enforce that tests don't just exist -- they pass, meet coverage thresholds, and survive fuzz testing.
3. Domain-Driven Design
The domain model is expressed through DDD attributes: [AggregateRoot], [Entity], [ValueObject], [Composition], [Command], [DomainEvent], [Saga]. Aggregate invariants are [Invariant] methods returning Result. The compiler validates DDD rules: every entity must be reachable via composition from an aggregate root, no cross-aggregate compositions, every aggregate must have at least one invariant.
[AggregateRoot]
public partial class Order
{
[EntityId] public partial OrderId Id { get; }
[Composition] public partial IReadOnlyList<OrderLine> Lines { get; }
[Composition] public partial Money Total { get; }
[Invariant("Order must have at least one line")]
private Result HasLines()
=> Lines.Count > 0
? Result.Success()
: Result.Failure("Order must have at least one line");
}[AggregateRoot]
public partial class Order
{
[EntityId] public partial OrderId Id { get; }
[Composition] public partial IReadOnlyList<OrderLine> Lines { get; }
[Composition] public partial Money Total { get; }
[Invariant("Order must have at least one line")]
private Result HasLines()
=> Lines.Count > 0
? Result.Success()
: Result.Failure("Order must have at least one line");
}4. Source Generation
From these attributes, Roslyn source generators produce the entire stack:
- Domain layer: Entity implementations, fluent builders, validation, invariant enforcement
- Persistence: EF Core DbContext, configurations, repositories, migrations
- API: REST controllers, GraphQL schema, OpenAPI documentation (enriched with requirement metadata)
- Admin: Blazor CRUD components (list, form, detail, batch actions)
- Frontend: Blazor WebAssembly page widgets
- Workflow: State machines with guard conditions
- Testing: Requirement registry, traceability matrix, compliance diagnostics
The developer writes ~50 lines of attributed partial classes. The compiler generates ~5,000 lines of production code. Everything is type-safe, debuggable, and version-controlled.
The Architecture at a Glance
The Solution Structure
Cmf.sln ← The framework itself
├── src/
│ ├── Cmf.Meta.Lib/ ← M3: MetaConcept, MetaProperty, etc.
│ ├── Cmf.Meta.Generators/ ← Stage 0: metamodel registry
│ ├── Cmf.Ddd.Lib/ ← M2: DDD DSL attributes
│ ├── Cmf.Ddd.Generators/ ← Stages 1-2: entities, CQRS, EF Core
│ ├── Cmf.Content.Lib/ ← M2: Content DSL
│ ├── Cmf.Content.Generators/ ← Stage 2: parts, blocks, StreamFields
│ ├── Cmf.Admin.Lib/ ← M2: Admin DSL
│ ├── Cmf.Admin.Generators/ ← Stage 3: Blazor admin components
│ ├── Cmf.Pages.Lib/ ← M2: Pages DSL
│ ├── Cmf.Pages.Generators/ ← Stage 3: page widgets, routing
│ ├── Cmf.Workflow.Lib/ ← M2: Workflow DSL
│ ├── Cmf.Workflow.Generators/ ← Stage 3: state machines
│ ├── Cmf.Requirements.Lib/ ← M2: Requirements DSL (base types)
│ ├── Cmf.Requirements.Generators/ ← Stages 2-4: registry, matrix, analyzers
│ ├── Cmf.Infrastructure.EfCore/ ← EF Core integration
│ ├── Cmf.Infrastructure.EfCore.Generators/ ← Stage 2-3: persistence generation
│ ├── Cmf.Cli/ ← CLI tool (cmf new, cmf add, cmf design)
│ └── Cmf.Templates/ ← dotnet new templates
└── test/
└── Cmf.*.Testing/ ← Test helpers per DSLCmf.sln ← The framework itself
├── src/
│ ├── Cmf.Meta.Lib/ ← M3: MetaConcept, MetaProperty, etc.
│ ├── Cmf.Meta.Generators/ ← Stage 0: metamodel registry
│ ├── Cmf.Ddd.Lib/ ← M2: DDD DSL attributes
│ ├── Cmf.Ddd.Generators/ ← Stages 1-2: entities, CQRS, EF Core
│ ├── Cmf.Content.Lib/ ← M2: Content DSL
│ ├── Cmf.Content.Generators/ ← Stage 2: parts, blocks, StreamFields
│ ├── Cmf.Admin.Lib/ ← M2: Admin DSL
│ ├── Cmf.Admin.Generators/ ← Stage 3: Blazor admin components
│ ├── Cmf.Pages.Lib/ ← M2: Pages DSL
│ ├── Cmf.Pages.Generators/ ← Stage 3: page widgets, routing
│ ├── Cmf.Workflow.Lib/ ← M2: Workflow DSL
│ ├── Cmf.Workflow.Generators/ ← Stage 3: state machines
│ ├── Cmf.Requirements.Lib/ ← M2: Requirements DSL (base types)
│ ├── Cmf.Requirements.Generators/ ← Stages 2-4: registry, matrix, analyzers
│ ├── Cmf.Infrastructure.EfCore/ ← EF Core integration
│ ├── Cmf.Infrastructure.EfCore.Generators/ ← Stage 2-3: persistence generation
│ ├── Cmf.Cli/ ← CLI tool (cmf new, cmf add, cmf design)
│ └── Cmf.Templates/ ← dotnet new templates
└── test/
└── Cmf.*.Testing/ ← Test helpers per DSLA developer using the CMF gets a different structure:
MyStore.sln ← The application
├── src/
│ ├── MyStore.Requirements/ ← Features as types with AC methods
│ ├── MyStore.SharedKernel/ ← Domain types (User, Role, etc.)
│ ├── MyStore.Specifications/ ← ISpec interfaces per feature
│ ├── MyStore.Lib/ ← Domain model ([AggregateRoot], etc.)
│ ├── MyStore.Infrastructure.Postgres/ ← EF Core + generated repos
│ ├── MyStore.Server/ ← ASP.NET host (API + admin + routing)
│ ├── MyStore.Client/ ← Blazor WASM (page widgets)
│ └── MyStore.Shared/ ← Shared kernel (DTOs, validation)
└── test/
└── MyStore.Tests/ ← Type-linked testsMyStore.sln ← The application
├── src/
│ ├── MyStore.Requirements/ ← Features as types with AC methods
│ ├── MyStore.SharedKernel/ ← Domain types (User, Role, etc.)
│ ├── MyStore.Specifications/ ← ISpec interfaces per feature
│ ├── MyStore.Lib/ ← Domain model ([AggregateRoot], etc.)
│ ├── MyStore.Infrastructure.Postgres/ ← EF Core + generated repos
│ ├── MyStore.Server/ ← ASP.NET host (API + admin + routing)
│ ├── MyStore.Client/ ← Blazor WASM (page widgets)
│ └── MyStore.Shared/ ← Shared kernel (DTOs, validation)
└── test/
└── MyStore.Tests/ ← Type-linked testsThe Five-Stage Pipeline
The CMF compiles through five stages, each building on the previous:
| Stage | Input | Output | Examples |
|---|---|---|---|
| 0 | M3 attributes on M2 attribute classes | Metamodel registry | MetamodelRegistry.g.cs |
| 1 | M1 model classes with DSL attributes | Validated model graph + diagnostics | Compiler errors for invalid models |
| 2 | Validated model graph | Domain code + persistence | Entities, builders, EF Core, repos |
| 3 | Stage 2 output | UI + API + workflows | Blazor admin, REST/GraphQL, state machines |
| 4 | All stages + test assemblies | Traceability + compliance | TraceabilityMatrix.g.cs, REQ diagnostics |
The Developer Experience
A developer's workflow with the CMF:
1. cmf new MyStore ← Scaffold project structure
2. cmf add aggregate Order ← Creates Order.cs with [AggregateRoot]
3. Define properties, compositions ← Add [Composition], [Property], etc.
4. Add [Invariant] methods ← Domain rules as Result-returning methods
5. cmf add command CreateOrder ← Creates command + handler scaffold
6. dotnet build ← Source generators run, code is produced
7. cmf design ← Interactive TUI to visualize and refine
8. Define features in Requirements/ ← Abstract records with AC methods
9. dotnet build ← Analyzer warns: "No spec for Feature X"
10. Create specifications ← ISpec interfaces
11. dotnet build ← Analyzer warns: "No impl for ISpec X"
12. Implement domain services ← : ISpec with [ForRequirement]
13. dotnet build ← Analyzer warns: "No tests for Feature X"
14. Write tests ← [Verifies] with typeof + nameof
15. dotnet test ← Tests execute
16. dotnet quality-gates check ← Pass rate, coverage, fuzz testing
17. dotnet run ← Full application running1. cmf new MyStore ← Scaffold project structure
2. cmf add aggregate Order ← Creates Order.cs with [AggregateRoot]
3. Define properties, compositions ← Add [Composition], [Property], etc.
4. Add [Invariant] methods ← Domain rules as Result-returning methods
5. cmf add command CreateOrder ← Creates command + handler scaffold
6. dotnet build ← Source generators run, code is produced
7. cmf design ← Interactive TUI to visualize and refine
8. Define features in Requirements/ ← Abstract records with AC methods
9. dotnet build ← Analyzer warns: "No spec for Feature X"
10. Create specifications ← ISpec interfaces
11. dotnet build ← Analyzer warns: "No impl for ISpec X"
12. Implement domain services ← : ISpec with [ForRequirement]
13. dotnet build ← Analyzer warns: "No tests for Feature X"
14. Write tests ← [Verifies] with typeof + nameof
15. dotnet test ← Tests execute
16. dotnet quality-gates check ← Pass rate, coverage, fuzz testing
17. dotnet run ← Full application runningAt every step, the compiler and analyzers guide the developer. Missing a specification? Compiler warning. Missing an implementation? Compiler warning. Missing a test? Compiler warning. Test fails? Quality gate fails. The chain is unbreakable.
What This Document Covers
The remaining eleven parts of this document detail every layer of this system:
- Part II defines the CMF's own features as a typed requirements catalog
- Parts III-VIII specify each of the six DSLs with complete C# examples
- Part IX details the type-safe requirements chain
- Part X covers Roslyn analyzers and quality gates
- Part XI describes the CLI and interactive TUI
- Part XII walks through building a complete e-commerce platform
Each part includes mermaid diagrams, full C# code examples, generated code samples, and architectural decision rationale. The goal is a document that an engineering team could use to build this system.
A Note on Implementation Status
Nothing in this document is implemented. This is a design specification -- a complete technical blueprint. The C# code examples show what the developer writes and what the compiler generates, but no compiler generates it yet. The mermaid diagrams show the architecture, but no architecture exists yet.
This is deliberate. The document validates the design before implementation begins. It answers the question: "If we built this, would it hold together?" By expressing the CMF's own requirements using the Requirements DSL notation, we validate the DSL design against a real-world project before the DSL itself is built.
The bootstrap question is addressed in Part II: once the CMF is built, could it redefine its own requirements using its own tooling? The answer is yes -- and that would be true self-hosting.