Heddle is a text template engine for .NET, written in C#. It compiles templates written in its small, purpose‑built language into reusable, strongly‑typed renderers. Heddle can embed real C# expressions, define and inherit reusable named blocks, compose output through extension chains, and render with full HTML‑encoding control.
The engine is published as a set of NuGet packages:
| Package | Project | Purpose |
|---|---|---|
Heddle |
src/Heddle | Core engine: parser host, compiler, runtime, built‑in extensions. |
Heddle.Language |
src/Heddle.Language | ANTLR grammar + generated lexer/parser, plus editor (Ace) assets. |
Heddle.Generator |
src/Heddle.Generator | Build‑time source generator that pre‑compiles .heddle files into your assembly. Reference with PrivateAssets="all" (an analyzer package). |
Heddle.LanguageServices |
src/Heddle.LanguageServices | Editor language‑service facade (completion, diagnostics, hover, go‑to‑definition) you can host yourself. |
Heddle.LanguageServer |
src/Heddle.LanguageServer | LSP server for editors, shipped as a dotnet tool (heddle-lsp). |
Heddle.Tool |
src/Heddle.Tool | The heddle CLI — a dotnet tool for rendering templates and build‑time code generation (the T4 successor). |
Current release line: 2.0.0. The published version is set from the latest
release tag (vX.Y.Z) at publish time —
see nuget.org — so the version in the source
tree is just a placeholder.
- Compiled and strongly typed. A template is compiled into an execution‑ready document —
extension calls wired to compiled accessors (member paths to expression‑tree delegates,
embedded C# to Roslyn delegates). With a typed model, member access and embedded C# are checked
at compile time rather than discovered at render time; declaring
@model(){{dynamic}}instead opts into render‑time member binding. - Fast. In this repository's benchmark run of 2026‑07‑11, Heddle rendered faster than ASP.NET Core Razor and allocated less memory (Razor's page is larger and not parity‑checked — the like‑for‑like comparison is against the four parity‑checked Liquid/Handlebars engines, which Heddle leads on render time and where it allocates the least or tied‑least memory — Handlebars.Net is within ~0.3 KB). See Architecture → Performance and the benchmark project.
- Composable without coupling. Reusable templates are declarative extension points, so a page can be split into independent pieces recombined by a layout — at no runtime cost — and any page can serve as a base for another. See Language Reference → inheritance.
- Extensible by design. The language has essentially one primitive — the extension call — so new directives are added as classes, not grammar. See Writing Custom Extensions.
Start with Getting Started, then read the
Language Reference for every construct and its behavioral
nuances, and Built‑in Extensions for the bundled helpers
(list, if, date, money, …). For the sandbox‑safe expression tier — operators, literals,
registered functions, ExpressionMode — see Native Expressions.
For task‑oriented idioms, see Patterns & Recipes, and for editor tooling see
Editor Support. Arriving from another engine? Start with
Coming from Razor or Coming from Liquid
(the latter also covers Jinja/Twig).
Read Getting Started and the C# API Reference
(HeddleTemplate, TemplateOptions, CompileContext, compile results, file watching). To compile
.heddle files into your assembly at build time, see
Build‑Time Pre‑compilation. For complete,
runnable integration examples — SSR, definition libraries, dynamic models, sandboxing, safe output,
custom extensions, component libraries, codegen, precompilation, streaming — see the
integration sample gallery. Replacing a Razor view layer? The
Coming from Razor guide maps the concepts across.
Read Writing Custom Extensions to add your own @yourhelper(...)
directives and register them via assembly attributes.
Read the Architecture (lex → parse → compile → render pipeline, Roslyn code generation, lexer modes) and Building & Testing.
| Document | What it covers |
|---|---|
| Getting Started | Install/build prerequisites and your first inline and file‑based templates. |
| Coming from Liquid | Migration guide for Liquid/Jinja/Twig authors: {{ }} as a body, filter chains, include/render, loops, tags, whitespace control. |
| Coming from Razor | Migration guide for ASP.NET Core Razor authors: relative context, layouts, @:, the three C# tiers, tag helpers → extensions. |
| Language Reference | Every Heddle construct: output, expressions, embedded C#, definitions, inheritance, subtemplates, chaining, imports, comments, raw blocks, whitespace, and the lexer‑mode model. |
| Native Expressions | The sandbox‑safe expression tier: operators, literals, registered functions, ExpressionMode. |
| Built‑in Extensions | Reference for every bundled extension, its expected input type, and HTML‑encoding behavior. |
| Patterns & Recipes | Task‑oriented idioms: with‑blocks, presence checks, first/last, local helper definitions, root context, JSON injection, and more. |
| C# API Reference | IHeddleTemplate/HeddleTemplate, TemplateOptions, CompileContext, HeddleCompileResult, registration, and error handling. |
| Build‑Time Pre‑compilation | Compiling .heddle files into the assembly: generator setup, typed entry points, registry, mismatch policy. |
| Writing Custom Extensions | The extension contract, Scope, attributes, and registration. |
| Architecture | Internal pipeline, ANTLR grammar, compilation, and editor tooling. |
| Syntax Highlighting | The portable TextMate grammar, its token→scope mapping, and how editors/sites consume it. |
| Editor Support | LSP server, VS Code extension, Neovim setup, configuration. |
| Building & Testing | SDK, build scripts, target frameworks, tests, packaging, and CI. |
@model(){{dynamic}}
<p>Hi @(Name) — you have @int(Count) new comments.</p>
HeddleTemplate.Configure(typeof(Program).Assembly);
var source = "@model(){{dynamic}}\n<p>Hi @(Name) — you have @int(Count) new comments.</p>";
using var template = new HeddleTemplate(source, new CompileContext(new TemplateOptions()));
string html = template.Generate(new Greeting { Name = "Ada", Count = 3 });
// => <p>Hi Ada — you have 3 new comments.</p>
// A named type, not an anonymous one: the runtime binder behind @(Name)/@int(Count) can't
// see another assembly's anonymous types.
public class Greeting { public string Name { get; set; } public int Count { get; set; } }