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 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 overwritten
  • YourGeneratedModule -- regenerated every time from the YAML schema
  • Module -- 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.yml was 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);
}

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);
}

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");
}

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

Diagram

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 DSL

A 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 tests

The Five-Stage Pipeline

The CMF compiles through five stages, each building on the previous:

Diagram
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 running

At 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.