Introduction -- CMF, Not CMS
In 2010, the Diem Project was a Content Management Framework built on Symfony 1.4 and Doctrine. It was not WordPress. It was not Drupal. It started empty -- no blog, no comments, no preinstalled modules. The 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. The frontend composed pages from Layouts, Zones, and Widgets. The backend auto-generated admin interfaces from the schema.
Diem was a CMF -- a Content Management Framework. The distinction matters.
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. Diem, Symfony CMF, and what this article describes all follow this philosophy.
Part 1: Introduction -- CMF, Not CMS
CMF vs CMS: Diem (PHP, 2010) as the origin story, and why a Content Management Framework starts empty and is generated from a domain definition.
Part 2: Architecture Overview
The two axes of the CMF -- domain modeling and content presentation -- and the multi-stage Roslyn source generation pipeline that compiles them.
Part 3: The DDD DSL (M2) -- Domain Modeling
AggregateRoot, Entity, ValueObject, Command, Event, Saga: the DDD DSL backbone and the Roslyn-generated entities, builders, validators, and CQRS plumbing.
Part 4: Composition, Ownership, and Persistence
Composition vs association vs aggregation, ownership rules, and how the generators turn them into EF Core mappings, repositories, and DbContext wiring.
Part 5: The Admin Module DSL (M2) -- Backend Management
AdminModule, AdminField, AdminFilter, AdminAction: a single attribute compiles to a complete Blazor CRUD admin interface with paginated lists and forms.
Part 6: Content Parts and Blocks DSL (M2) -- Composable Content
Content parts as horizontal composition (Routable, Seoable, Taggable) and content blocks as vertical composition, including StreamFields and versioning.
Part 7: The Page/Widget DSL (M2) -- Content Presentation
Page tree, Layouts, Zones, and PageWidget attributes that compile to Blazor WebAssembly components with dynamic URL routing from materialized paths.
Part 8: Cross-Cutting CMS Concerns and the Workflow DSL (M2)
Workflow, Stage, Transition, Gate, RequiresRole: per-locale editorial pipelines, scheduled publishing, and generated state machines with guard conditions.
Part 9: What Gets Generated (Stages 2+3)
A walk through the artifacts produced by the Roslyn pipeline at Stages 2 and 3 -- from metadata classes to runtime services -- and how they fit together.
Part 10: The Shared Kernel -- Blazor WASM + Server
A single C# kernel compiled to both Blazor Server and Blazor WebAssembly so domain types, validation, and contracts stay identical across the stack.
Part 11: CLI Tooling
cmf new, cmf add, cmf generate, cmf validate, cmf report -- the developer-facing surface that drives scaffolding, regeneration, and quality checks.
Part 12: Complete Example -- E-Commerce
A worked end-to-end example: defining an e-commerce domain in attributes and watching the generators emit entities, admin UI, pages, and APIs.
Part 13: The Requirements/Feature Tracking DSL
Typed requirement references, hierarchical feature tracking, lambda-based acceptance criteria, and automatic tracing from features to code, tests, and API docs.
Part 14: The CMF Landscape -- Standing on Giants
What this CMF borrows from Diem, Wagtail, Orchard Core, Drupal, Symfony CMF, and Strapi, why each pattern was chosen against its alternatives, the failure modes the CMF is designed to avoid, and a migration path from Diem 1.x.
Part 15: Testing the Generated Stack
A seven-layer testing strategy for a CMF where most code is generated: aggregate invariants, command handlers, EF integration, Blazor components with bUnit, exhaustive workflow theories, generated REST/GraphQL endpoints, and the requirements traceability chain enforced at compile time.
Part 16: Security, Authorization & Audit
How a single [RequiresRole] propagates to controllers, Blazor buttons, GraphQL fields, audit entries and policy registrations. Multi-tenant isolation, claims-based policies, payload-hashed audit trails, and the generated permission matrix.
Part 17: Extending the M3 with a New DSL
How to ship a seventh DSL as a NuGet package: anatomy of a DSL, the five-stage hooks third-party generators consume, two case studies (a Permissions DSL and a Hierarchical Tenant DSL), and the rules for additive vs augmenting generators.
Part 18: Performance, Caching & Read Models
Eager-load defaults, build-time query profiling with budgets, [Cacheable] queries with event-driven invalidation, materialized read-model aggregates fed by Stage 4 projections, and search indexing on domain events. The escape hatches for hot paths.
Part 19: Summary
The four-layer M0/M1/M2/M3 picture, what the nineteen parts built, what the CMF deliberately leaves out, the self-hosting roadmap, and where to read more.