Part VII: The Diem Instance --- 200 Lines of Content Model
tspec's server is a Diem instance. Like a blog is a WordPress instance. ~200 lines of C# content model. Diem generates everything else.
Every SaaS product reaches the moment where someone asks: "So how long did the dashboard take to build?" And the answer is usually measured in months. The API took a sprint. The database migrations took a week. The admin UI took three sprints. The search indexing took another sprint. The workflow engine --- don't ask.
tspec's answer is different. The dashboard, the API, the database schema, the workflow engine, the search --- all of it comes from roughly 200 lines of C# decorated with Diem attributes. Diem is a Content Management Framework built on Roslyn source generators. You describe your domain. Diem generates the infrastructure. tspec is one instance of that framework, the same way a cooking blog is one instance of WordPress.
The Split
tspec has two halves, and they run in different places.
Language backends run on developer machines. The C# backend runs inside the compiler via Roslyn. The TypeScript backend runs as a CLI tool. The Rust backend runs as a proc macro. They scan code, resolve types, produce JSON. They live where the code lives.
The Diem instance runs centrally --- cloud-hosted, on-premise, or both. It receives JSON from backends, stores it, versions it, visualizes it, manages workflow states, and serves the Blazor dashboard. It does not know or care which language produced the scan. It only knows its own content model.
The split is clean. Backends push. Diem stores and serves. No backend needs to understand the dashboard. No dashboard page needs to understand Roslyn or syn or the TypeScript compiler API.
What Diem Provides Out of the Box
Diem is not a single library. It is a suite of DSLs, each handling one concern, all compiled together by Roslyn source generators at build time. The CMF series covers each in detail:
- DDD DSL --- aggregates, entities, value objects, repositories, CQRS command/query separation. You mark a class
[AggregateRoot]and get a full repository with queries, commands, and persistence. - Content DSL --- content types with versioning, draft/publish lifecycle, localization. Every entity gets an audit trail for free.
- Admin DSL ---
[AdminModule]on any aggregate root auto-generates a Blazor CRUD interface: paginated lists, column filters, detail views, create/edit forms, batch operations. - Workflow DSL --- state machines with named transitions and gate evaluation. Mark a class
[Workflow("Name")]and get a full state engine with transition validation. - Pages DSL --- dashboard pages composed from widgets. Charts, tables, KPI cards --- all declared, all generated.
- Requirements DSL --- type-safe requirement chains with compile-time verification.
- Roslyn Source Generators --- five-stage pipeline (Stages 0--5). Everything compiles at build time. No runtime reflection. No code-behind files to maintain.
- API Layer --- REST endpoints auto-generated from the domain model. One aggregate root = one set of CRUD endpoints + custom actions.
tspec uses all of them. The content model below is the only code the tspec server needs.
The tspec Content Model (~200 Lines)
Here is the actual domain model. Every class, every attribute, every property. This is not a simplified example. This is the production code.
namespace Tspec.Domain;
// ── Aggregates ──
[AggregateRoot]
[AdminModule(ListColumns = new[] { "Name", "RepoCount", "Flavor" })]
public partial class Project
{
[Required] public string Name { get; set; } = "";
public string Description { get; set; } = "";
[ValueObject] public FlavorConfig Flavor { get; set; } = new();
[Composition] public ICollection<Repository> Repositories { get; set; }
= new List<Repository>();
[Composition] public ICollection<TeamMember> Members { get; set; }
= new List<TeamMember>();
}
[AggregateRoot]
[AdminModule(ListColumns = new[] {
"Project", "Branch", "Commit", "Timestamp", "Coverage"
})]
public partial class Scan
{
[Association] public Project Project { get; set; } = null!;
[Required] public string Branch { get; set; } = "";
[Required] public string Commit { get; set; } = "";
public DateTime Timestamp { get; set; }
public string Language { get; set; } = "";
public int TotalFeatures { get; set; }
public int TotalACs { get; set; }
public int CoveredACs { get; set; }
public decimal CoveragePercentage { get; set; }
[Composition] public ICollection<FeatureSnapshot> Features { get; set; }
= new List<FeatureSnapshot>();
}
[Entity]
public partial class FeatureSnapshot
{
[Required] public string FeatureId { get; set; } = "";
public string Title { get; set; } = "";
public string Priority { get; set; } = "";
public string Level { get; set; } = "";
public string? ParentId { get; set; }
public int TotalACs { get; set; }
public int CoveredACs { get; set; }
[Composition] public ICollection<ACSnapshot> AcceptanceCriteria { get; set; }
= new List<ACSnapshot>();
}
[ValueObject]
public partial class ACSnapshot
{
public string Name { get; set; } = "";
public string? Description { get; set; }
public bool Covered { get; set; }
public string? TestFile { get; set; }
public string? TestName { get; set; }
}
[AggregateRoot]
[AdminModule]
[Workflow("FeatureLifecycle")]
public partial class FeatureState
{
[Association] public Project Project { get; set; } = null!;
[Required] public string FeatureId { get; set; } = "";
public string CurrentStage { get; set; } = "Draft";
[Association] public TeamMember? Assignee { get; set; }
[Composition] public ICollection<Comment> Comments { get; set; }
= new List<Comment>();
}
[Entity]
public partial class TeamMember
{
[Required] public string Email { get; set; } = "";
public string DisplayName { get; set; } = "";
public TeamRole Role { get; set; } = TeamRole.Developer;
}
public enum TeamRole { Admin, ProjectOwner, Developer, QA, PM, Viewer }namespace Tspec.Domain;
// ── Aggregates ──
[AggregateRoot]
[AdminModule(ListColumns = new[] { "Name", "RepoCount", "Flavor" })]
public partial class Project
{
[Required] public string Name { get; set; } = "";
public string Description { get; set; } = "";
[ValueObject] public FlavorConfig Flavor { get; set; } = new();
[Composition] public ICollection<Repository> Repositories { get; set; }
= new List<Repository>();
[Composition] public ICollection<TeamMember> Members { get; set; }
= new List<TeamMember>();
}
[AggregateRoot]
[AdminModule(ListColumns = new[] {
"Project", "Branch", "Commit", "Timestamp", "Coverage"
})]
public partial class Scan
{
[Association] public Project Project { get; set; } = null!;
[Required] public string Branch { get; set; } = "";
[Required] public string Commit { get; set; } = "";
public DateTime Timestamp { get; set; }
public string Language { get; set; } = "";
public int TotalFeatures { get; set; }
public int TotalACs { get; set; }
public int CoveredACs { get; set; }
public decimal CoveragePercentage { get; set; }
[Composition] public ICollection<FeatureSnapshot> Features { get; set; }
= new List<FeatureSnapshot>();
}
[Entity]
public partial class FeatureSnapshot
{
[Required] public string FeatureId { get; set; } = "";
public string Title { get; set; } = "";
public string Priority { get; set; } = "";
public string Level { get; set; } = "";
public string? ParentId { get; set; }
public int TotalACs { get; set; }
public int CoveredACs { get; set; }
[Composition] public ICollection<ACSnapshot> AcceptanceCriteria { get; set; }
= new List<ACSnapshot>();
}
[ValueObject]
public partial class ACSnapshot
{
public string Name { get; set; } = "";
public string? Description { get; set; }
public bool Covered { get; set; }
public string? TestFile { get; set; }
public string? TestName { get; set; }
}
[AggregateRoot]
[AdminModule]
[Workflow("FeatureLifecycle")]
public partial class FeatureState
{
[Association] public Project Project { get; set; } = null!;
[Required] public string FeatureId { get; set; } = "";
public string CurrentStage { get; set; } = "Draft";
[Association] public TeamMember? Assignee { get; set; }
[Composition] public ICollection<Comment> Comments { get; set; }
= new List<Comment>();
}
[Entity]
public partial class TeamMember
{
[Required] public string Email { get; set; } = "";
public string DisplayName { get; set; } = "";
public TeamRole Role { get; set; } = TeamRole.Developer;
}
public enum TeamRole { Admin, ProjectOwner, Developer, QA, PM, Viewer }Count the lines. Six classes, one enum, a handful of attributes. That is the entire tspec server domain.
What Diem Generates From This
From those ~80 lines of meaningful code (ignoring braces and whitespace), Diem's source generators produce:
- REST API ---
POST /api/scans,GET /api/projects,GET /api/projects/:id,GET /api/features,PATCH /api/features/:id/state,GET /api/scans?branch=main&commit=abc123, and more. Full CRUD for every aggregate root. Custom query endpoints for associations. - Blazor Admin --- paginated list views with sortable columns matching the
ListColumnsarrays. Detail views with nested entity display. Create and edit forms with validation. Filters on every column type. Batch operations (delete, reassign, change state). - Workflow Engine --- a full state machine for
FeatureLifecyclewith named transitions (Draft to Proposed, Proposed to Approved, and so on), gate evaluation (who can transition?), and audit logging of every state change. - EF Core Migrations --- PostgreSQL schema with proper foreign keys, indexes on
[Required]fields, JSON columns for value objects, and migration history. - Repositories --- CQRS command/query handlers for every aggregate root.
CreateProjectCommand,UpdateScanCommand,GetProjectByIdQuery,ListScansQuerywith pagination and filtering --- all generated. - Validators --- input validation from
[Required], type constraints, and association integrity checks. AScancannot reference aProjectthat does not exist. - Full-Text Search --- search across all
[AdminModule]-decorated entities. Type "navigation" and get matching projects, features, and scans in one result set.
None of this code is written by hand. None of it needs maintenance. Change the model, rebuild, and the generated code updates.
The Power of Attribute-Driven Generation
Consider what happens when a new field is needed. Say we want to track which CI pipeline produced a scan. The change is one line:
public string? PipelineId { get; set; }public string? PipelineId { get; set; }One property. On rebuild, Diem regenerates:
- The API endpoint now accepts and returns
pipelineId - The admin list can include a "PipelineId" column
- The admin form gets a new text field
- The admin filters support filtering by pipeline
- The database migration adds the column
- The repository queries include the field
- The search index covers the new value
No migration script to write. No API controller to update. No form component to add. No search configuration to edit. One line of C# produces seven infrastructure changes.
Compare this to building from scratch. A REST API for six entities with full CRUD, filtering, pagination, and nested resources: conservatively 10,000 lines of controller and service code. A Blazor admin with lists, forms, detail views, and batch operations: another 5,000 lines. Database migrations, repository implementations, workflow state machines, search indexing, input validation --- the total easily passes 20,000 lines.
tspec replaces that with ~200 lines and a build step.
Before and After
What tspec looked like as a standalone scanner, versus what it looks like as a Diem instance:
Before --- standalone scanner, no persistence:
After --- Diem instance, full platform:
The scanner still exists. The backends still produce JSON. But now that JSON flows into a system that stores it, versions it, visualizes it, and manages its lifecycle. The 200-line content model is the pivot point. Everything above it (the backends) pushes data in. Everything below it (the generated infrastructure) serves data out.
That is the Diem instance. Not a custom-built server. Not a hand-rolled API. A content model, a framework, and a build step.
Previous: Part VI: Developer Experience Next: Part VIII: Dashboard Deep-Dive