Templates
Templates
Templates define how domain definitions are transformed into output code. They are the "rules" that AI agents and the generation engine use to produce final output.
Key Decisions
| Decision | Choice |
|---|---|
| Template format | Text files (Scriban), NOT compiled C# |
| Location | External files, editable without compilation |
| AI access | AI can create, modify, and consume templates |
| Separation | Templates separate from definitions |
| Traceability | Generated code includes generation metadata |
Previous Approach (Replaced)
The original implementation used C# classes with StringBuilder to build output:
public IOutput Create()
{
var sb = new StringBuilder();
sb.Al("namespace MyNamespace;");
sb.B();
sb.Al($"public class {_entity.Name}");
sb.Al("{");
foreach (var field in _entity.Fields)
{
sb.I(1).Al($"public {field.DataType} {field.Name} {{ get; set; }}");
}
sb.Al("}");
return new File($"{_entity.Name}.cs", sb.ToString());
}This approach was replaced with Scriban because:
| Issue | Impact |
|---|---|
| Hard to read | Output structure not visible at a glance |
| Verbose | Lots of boilerplate (.Al(), .I(), .B()) |
| Mixed concerns | Logic and output intertwined |
| Requires compilation | Can't modify templates without rebuilding |
| Language-specific | Hard to create templates for multiple output languages |
| Not reusable | Similar patterns repeated across templates |
Current Approach: Scriban
Scriban is a fast, powerful, and safe text templating engine for .NET.
Why Scriban
| Feature | Benefit |
|---|---|
| Text-based templates | Edit without recompilation |
| Fast & lightweight | Minimal overhead, GC-friendly |
| Full scripting | if/else/for/while, expressions, functions |
| Liquid-compatible | Can parse Liquid syntax if needed |
| Extensible | Custom functions, member renaming |
| Safe sandbox | Control what objects are exposed |
| VS Code extension | Syntax highlighting available |
| Async support | Template.RenderAsync for async operations |
Template Syntax
{{~ # Scriban uses {{ }} delimiters ~}}
namespace {{ enterprise.namespace }};
public class {{ entity.name }}
{
{{~ for field in entity.fields ~}}
public {{ field.data_type }} {{ field.name }} { get; set; }
{{~ end ~}}
}Installation
dotnet add package ScribanBasic Usage
using Scriban;
// Load template from file (no compilation needed!)
var templateText = File.ReadAllText("templates/entity.scriban");
var template = Template.Parse(templateText);
// Render with domain model
var output = template.Render(new {
Entity = entity,
Enterprise = enterprise
});Generation Metadata (Traceability)
Generated code must contain enough information to regenerate it. This enables:
- Re-running generation when definitions change
- Understanding what created a file
- Knowing whether a file can be safely overwritten
Current Header Pattern
The existing Header template provides this (keep this pattern!):
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a Modeller template:
// Template: csharp/entity.scriban
// Definition: definitions/booking/entity.yaml
// Generated: 2024-12-03T10:30:00Z
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------Scriban Header Template
{{~ # _header.scriban - Include at top of every generated file ~}}
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a Modeller template:
// Template: {{ generation.template_path }}
// Definition: {{ generation.definition_path }}
// Generated: {{ generation.timestamp | date.to_string '%Y-%m-%dT%H:%M:%SZ' }}
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable {{ options.nullable | default 'enable' }}Generation Context
Every template receives a generation object:
generation:
template_path: "templates/csharp/entity.scriban"
template_version: "1.0.0"
definition_path: "definitions/booking/entity.yaml"
timestamp: "2024-12-03T10:30:00Z"
can_overwrite: trueTemplate Organisation
Templates are stored in a dedicated folder, separate from definitions:
project/
├── definitions/ # Domain definitions (DSL/YAML)
│ ├── booking/
│ │ ├── entity.yaml
│ │ └── commands.yaml
│ └── ...
│
├── templates/ # Scriban templates (editable files)
│ ├── _shared/ # Shared includes
│ │ ├── _header.scriban
│ │ ├── _property.scriban
│ │ └── _using.scriban
│ │
│ ├── csharp/ # C# output templates
│ │ ├── template.yaml # Template metadata
│ │ ├── entity.scriban
│ │ ├── repository.scriban
│ │ └── command-handler.scriban
│ │
│ ├── typescript/ # TypeScript output templates
│ │ ├── template.yaml
│ │ ├── interface.scriban
│ │ └── service.scriban
│ │
│ ├── sql/ # SQL output templates
│ │ ├── template.yaml
│ │ └── create-table.scriban
│ │
│ └── documentation/ # Documentation templates
│ ├── template.yaml
│ └── entity-docs.scriban
│
└── output/ # Generated files (with metadata headers)
└── ...Template Composition
Small, reusable template fragments:
templates/
├── csharp/
│ ├── _header.scriban
│ ├── _property.scriban
│ ├── _class.scriban
│ ├── entity.scriban # uses _class, _property
│ └── repository.scriban
├── typescript/
│ ├── _property.scriban
│ ├── interface.scriban
│ └── service.scriban
---
## Template Definition Format
Templates should be described in a discoverable format:
```yaml
# templates/csharp/entity.template.yaml
template: CSharpEntity
version: 1.0
description: Generates a C# entity class from a domain entity
input:
requires:
- entity
optional:
- enterprise
output:
type: file
extension: .cs
naming: "{{ entity.name }}.cs"
options:
nullable: true
use_records: false
generate_constructor: true
files:
- entity.scriban
includes:
- _header.scriban
- _property.scribanMulti-Language Support
Same domain definition, multiple output languages:
# Template set for an Entity
template_set: Entity
description: Entity representations across languages
variants:
- language: csharp
template: csharp/entity.scriban
extension: .cs
- language: typescript
template: typescript/interface.scriban
extension: .ts
- language: python
template: python/dataclass.scriban
extension: .py
- language: sql
template: sql/create-table.scriban
extension: .sqlLanguage-Specific Helpers
Each language gets type mappings and conventions:
# languages/csharp.yaml
language: csharp
file_extension: .cs
type_mappings:
text: string
integer: int
decimal: decimal
boolean: bool
date: DateOnly
datetime: DateTime
guid: Guid
conventions:
class_naming: PascalCase
property_naming: PascalCase
field_naming: _camelCase
nullable_syntax: "{{ type }}?"
collection_syntax: "List<{{ type }}>"Example: Entity Template (Scriban)
{{~ # Entity template for C# ~}}
{{~ include '_header.scriban' ~}}
namespace {{ enterprise.namespace }}.{{ service.name }}.Entities;
/// <summary>
/// {{ entity.description }}
/// </summary>
public class {{ entity.name }}
{
{{~ for field in entity.fields ~}}
/// <summary>
/// {{ field.description }}
/// </summary>
public {{ field | to_csharp_type }} {{ field.name }} { get; set; }{{ if field.default_value }} = {{ field.default_value }};{{ end }}
{{~ end ~}}
{{~ if entity.belongs_to ~}}
// Navigation property
public {{ entity.belongs_to }} {{ entity.belongs_to }} { get; set; } = default!;
{{~ end ~}}
}Template Functions
Custom functions available in templates:
| Function | Description | Example |
|---|---|---|
to_csharp_type | Convert domain type to C# | {{ field | to_csharp_type }} |
to_typescript_type | Convert to TypeScript | {{ field | to_typescript_type }} |
pascal_case | Convert to PascalCase | {{ name | pascal_case }} |
camel_case | Convert to camelCase | {{ name | camel_case }} |
snake_case | Convert to snake_case | {{ name | snake_case }} |
pluralize | Pluralize a word | {{ name | pluralize }} |
singularize | Singularize a word | {{ name | singularize }} |
AI and Templates
AI agents can interact with templates in two ways:
1. Consume Templates (as Rules)
AI uses existing templates to generate code from definitions:
AI Agent → reads definition → applies template → produces outputThe template acts as a "rule" the AI follows for consistent output.
2. Create/Modify Templates
AI can author new templates or improve existing ones:
User: "Create a template for generating Python dataclasses from entities"
AI: Creates templates/python/dataclass.scribanThis enables rapid creation of output formats without manual template authoring.
Remaining Questions
-
Versioning: How to handle template version upgrades? Semantic versioning?
-
Validation: How to validate templates produce syntactically correct output?
-
Testing: How to test templates in isolation? Golden file comparisons?
-
Discovery: How does the engine find and list available templates?
-
Dependencies: Can templates depend on other templates (beyond includes)?