Code generators are easy to dismiss as glorified string concatenation — some if statements and a few for loops stitched together with printf. Jumpstart is not that. Understanding how its generator actually works reveals a thoughtfully engineered pipeline with a proper in-memory model, a real templating engine, and a graph algorithm borrowed from compiler theory. This post opens the hood.
The Loading Pipeline
Before a single line of code is generated, Jumpstart builds a complete in-memory representation of your application from three CSV sources.
CSVLoader reads your application’s metadata CSV. This is the file you write: one row per column, describing every table, every column’s data type, every foreign key relationship.
CoreLoader then merges in templates/core/core.csv — the built-in system tables that every generated application receives. Organizations, principals, role-based access control, the workflow engine, script execution infrastructure — all of it is defined here and quietly merged into your model.
GlobalCSVLoader reads templates/core/global.csv, which defines audit columns — is_active, created_by, last_updated, last_updated_by, version — and adds them to every non-view table in the model.
The result of all three loaders is a populated MetaModel object. Then .Process() runs.
MetaModel.Process(): Building the Relationships
The raw CSV data tells you what tables and columns exist. Process() figures out how they relate to each other.
ProcessChildren
This pass scans every attribute with FK_TYPE="parent" and registers reverse relationships. If exec_log.workflow_id has FK_TYPE=parent pointing to workflow, then workflow.Children gets an entry for exec_log. This is what drives the /api/workflow/children/{id}?child=execlog endpoint and the child record tabs on the Blazor edit page — neither requires any hand-written configuration.
ProcessEnumObjects
Enum FKs point to lookup tables. This pass finds each referenced table’s Real World Key (RWK) column and synthesizes a display-name column on the view model. If workflow.workflow_type_id references workflow_type, and workflow_type has a column name marked as RWK, then ProcessEnumObjects synthesizes a workflow_type_name column on WorkflowView. The templates then use this synthesized column to generate dropdown selects in forms and human-readable values in list views.
ProcessView
Tables with names ending in _view are treated as SQL views. This pass recursively follows all FK chains to gather RWK columns from referenced tables and builds a JOIN structure. If customer_view references customer, and customer has a org_id FK to org, and org has an RWK column name, then ProcessView synthesizes org_name on customer_view and registers a LEFT OUTER JOIN org ON customer.org_id = org.id. The database template renders this directly into a CREATE VIEW statement.
The MetaModel Class Hierarchy
The in-memory model is a clean class hierarchy:
MetaBaseElement (base: Name, FileName)
├── MetaAttribute (a single column)
├── MetaObject (a table or view)
├── MetaSchema (a database schema grouping)
├── MetaModel (root: all schemas, objects, relationships)
└── MetaBuild (output file tracking)
MetaObject is the workhorse. It holds the table’s PascalCase class name (Customer), its lowercase variable name (customer), its uppercase constant (CUSTOMER), its view class name (CustomerView), its schema, its navigation menu group, its custom URI, and the fully resolved lists of attributes, children, enum relationships, and view join structures.
MetaAttribute tracks everything about a single column: the raw SQL type, the SQL Server equivalent, the .NET type, the UI input control type, the data reader conversion method, whether it’s nullable, what kind of FK it is, and what table it references. All of this is precomputed so that templates can be as simple as possible.
Topological Sort via Tarjan’s Algorithm
Once the model is fully processed, the generator sorts MetaObjects by dependency order before generating anything. Parent tables must come before child tables in DDL (you can’t create a foreign key to a table that doesn’t exist yet), and seed data must insert parent records before child records.
Jumpstart uses Tarjan’s strongly connected components (SCC) algorithm to produce this ordering. Tarjan’s is a depth-first search algorithm that finds all SCCs in a directed graph in a single pass — O(V + E) time. For a dependency graph with no cycles (which a well-formed relational model should have), each SCC is a single node, and the reverse of the SCC discovery order is a valid topological sort.
Using a proper graph algorithm rather than a naive multi-pass sort matters when models are large. It also handles the theoretical case of circular references gracefully, identifying them rather than looping forever.
The Generation Pipeline
With a sorted model in hand, the generator runs four passes:
GenerateApp() → model-type templates (one output file each)
GenerateSchemas() → schema-type templates (one per database schema)
GenerateObjects() → object-type templates (one per domain object)
GenerateBuild() → build-type templates (one output file each)
Each pass reads from a template registry — a CSV file that lists which templates to run, where the templates live, where to write output, and whether to apply the FORCE flag.
Template Types
| Type | Model Object | Runs | Typical Outputs |
|---|---|---|---|
model | MetaModel | Once | Config files, solution files, shared infrastructure |
schema | MetaSchema | Per schema | Schema-level DDL |
object | MetaObject | Per table/entity | Domain classes, controllers, logic, tests |
build | MetaBuild | Once at end | Build scripts, solution files |
The object type is the most prolific. For each table in your model, the generator runs every object-type template — producing the domain class, the generated and user partial classes, the logic layer, the API controller, the Blazor list page, the Blazor edit form, the persist layer, and the test classes.
RazorLight: Real Templating, Not String Concatenation
Templates are .cshtml files rendered by RazorLight, a Razor-based templating engine that runs outside of ASP.NET. This means templates have access to the full Razor syntax — @foreach, @if, @{ } code blocks, helper methods — with the MetaObject (or MetaModel, depending on type) as @Model.
A fragment of a domain class template looks like this:
public partial class @(Model.DomainObj)
{
@foreach (var attr in Model.UserAttributes)
{
<text> public @attr.DotNetType @attr.PascalName { get; set; }</text>
}
}
The generator post-processes the rendered output to handle a few special escape sequences. Because Razor treats @ as its own directive character, Blazor’s @ directives (like @page, @inject) are written as &at; in templates and substituted after rendering.
Output Filename Convention
Output filenames are derived from template filenames by a simple rule:
outputFile = templateFile.Replace("template", model.FileName).Replace(".cshtml", "")
So template.generated.cs.cshtml rendered for the Customer object produces Customer.generated.cs. templateLogic.generated.cs.cshtml produces CustomerLogic.generated.cs. Templates without “template” in the name keep their original name (minus .cshtml) — useful for files like Program.cs or appsettings.json that are the same regardless of the model.
The FORCE Flag
The FORCE column in template registries controls whether a file is overwritten on regeneration:
- FORCE=TRUE — Always regenerated. Used for
*.generated.csfiles. Never hand-edit these. - FORCE=FALSE — Created once, never touched again. Used for
*.user.csfiles. This is where you write your business logic.
This is the mechanism that makes Jumpstart practical for long-lived projects. You can evolve your data model iteratively — adding tables, adding columns, changing relationships — and regenerate freely, knowing your custom code is safe.
Template Registries
The three template registries — server-dotnet.csv, web-blazor.csv, and database-pgsql.csv / database-mssql.csv — collectively define around 150 templates. The server registry alone covers the full .NET stack: common infrastructure, domain classes, persistence, logic, API controllers, the scheduling server, the agent server, and all test projects. The web registry covers Blazor pages, reusable components, layout, and static assets.
Adding a new code generation target is as simple as writing a new .cshtml template and adding a row to the appropriate registry CSV. The generator doesn’t need to know anything about the new template — it reads the registry at runtime.
The Result
In under a second, the generator reads your CSV, resolves every relationship, sorts every object, and renders every template. What comes out the other side isn’t a rough sketch — it’s a structured, layered, compilable application that follows consistent patterns throughout. Every entity has the same shape. Every API follows the same conventions. Every test uses the same fixtures.
That consistency is itself the point. When a human writes boilerplate by hand across a large model, inconsistency creeps in. The generator never has a bad day.