Modeller
ArchitectureLegacy Design Drafts

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

DecisionChoice
Template formatText files (Scriban), NOT compiled C#
LocationExternal files, editable without compilation
AI accessAI can create, modify, and consume templates
SeparationTemplates separate from definitions
TraceabilityGenerated 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:

IssueImpact
Hard to readOutput structure not visible at a glance
VerboseLots of boilerplate (.Al(), .I(), .B())
Mixed concernsLogic and output intertwined
Requires compilationCan't modify templates without rebuilding
Language-specificHard to create templates for multiple output languages
Not reusableSimilar patterns repeated across templates

Current Approach: Scriban

Scriban is a fast, powerful, and safe text templating engine for .NET.

Why Scriban

FeatureBenefit
Text-based templatesEdit without recompilation
Fast & lightweightMinimal overhead, GC-friendly
Full scriptingif/else/for/while, expressions, functions
Liquid-compatibleCan parse Liquid syntax if needed
ExtensibleCustom functions, member renaming
Safe sandboxControl what objects are exposed
VS Code extensionSyntax highlighting available
Async supportTemplate.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 Scriban

Basic 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: true

Template 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.scriban

Multi-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: .sql

Language-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:

FunctionDescriptionExample
to_csharp_typeConvert domain type to C#{{ field | to_csharp_type }}
to_typescript_typeConvert to TypeScript{{ field | to_typescript_type }}
pascal_caseConvert to PascalCase{{ name | pascal_case }}
camel_caseConvert to camelCase{{ name | camel_case }}
snake_caseConvert to snake_case{{ name | snake_case }}
pluralizePluralize a word{{ name | pluralize }}
singularizeSingularize 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 output

The 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.scriban

This enables rapid creation of output formats without manual template authoring.


Remaining Questions

  1. Versioning: How to handle template version upgrades? Semantic versioning?

  2. Validation: How to validate templates produce syntactically correct output?

  3. Testing: How to test templates in isolation? Golden file comparisons?

  4. Discovery: How does the engine find and list available templates?

  5. Dependencies: Can templates depend on other templates (beyond includes)?

On this page