Template syntax

edit

A template is text with code blocks. By default, a code block starts with <% and ends with %>; the character after <% gives its kind:

BlockPurposeExample
TextWritten to the output as isHello World
<%= expression %>Writes the value of a C# expression<%= 2 + 2 %>
<% statements %>Runs C# statements<% var x = 10; %>
<%+ member %>Declares a member of the generated class<%+ private static int Get() => 42; %>
<%@ name value %>A directive: imports a namespace, references an assembly, …<%@ using System.IO %>
<%<%Writes <%<%<%= Name %>

The code is C#, compiled with the latest version of the language that the compiler supports.

Write a value

<%= expression %> writes the value of the expression:

using var template = new Template();
template.Load("2 + 2 = <%= 2 + 2 %>");
var result = template.Run();
Assert.Equal("2 + 2 = 4", result);
  • A null value writes nothing.
  • Nothing is encoded: a value that contains < or & is written as is. To generate HTML, use HTML emails, whose blocks encode values.
  • Numbers and dates are formatted with the culture of the writer. Run writes to a StringWriter that uses the current culture of the thread; to get the same text on every machine, write to a writer that uses the invariant culture:
using var template = new Template();
template.Arguments.Add(new TemplateArgument("Total", typeof(decimal)));
template.Load("Total: <%= Total %>");

using var writer = new StringWriter(CultureInfo.InvariantCulture);
template.Run(writer, 1234.5m);
Assert.Equal("Total: 1234.5", writer.ToString());

Run statements

The code of <% ... %> blocks is pasted, in order, into the body of the method that renders the template, between the instructions that write the text. A block can open a loop or a condition and a later block can close it:

using var template = new Template();
template.Load("""
    <% for(int i = 1; i <= 3; i++) { %>
      Item <%= i %>: <%= i * 10 %>
    <% } %>
    """);
var result = template.Run();
Assert.Equal("""
      Item 1: 10
      Item 2: 20
      Item 3: 30

    """, result);

A statement block can also write to the output directly: the output is a parameter named __output__ (the OutputParameterName property), and __output__.Write(value) writes a value.

Declare members

<%+ ... %> adds fields, properties or methods to the class generated for the template. The template code runs in a static method, so the members it uses must be static:

using var template = new Template();
template.Load("""
    <%+private static string Prefix() => "Item"; %>
    <%= Prefix() %> 1
    """);
var result = template.Run();
Assert.Equal("Item 1", result);

The + must follow the delimiter immediately: <% + ... %> is a statement block.

Directives

<%@ name value %> configures the compilation of the template. The name is case-insensitive:

DirectiveEffect
usingImports a namespace or declares an alias, like Usings.Add
referenceReferences an assembly by path; alias=Name path declares an extern alias
includeCompiles a C# source file with the template
inheritsSets the base class of the generated class (BaseClassFullTypeName)
implementsAdds interfaces, separated by commas, to the generated class

Using code and libraries shows each of them. Other directives write nothing and change nothing, but your code can read them from Blocks; the command-line tool reads outputextension this way.

Line breaks

A statement block, a member block or a directive that is alone on its line removes the line break that follows it, so the lines that only hold code do not leave empty lines in the output:

using var template = new Template();
template.Load("""
    <ul>
    <% foreach (var item in new[] { "Apples", "Pears" }) { %>
      <li><%= item %></li>
    <% } %>
    </ul>
    """);

var result = template.Run();
Assert.Equal("""
    <ul>
      <li>Apples</li>
      <li>Pears</li>
    </ul>
    """, result);
  • The spaces after the block are removed with the line break, but the spaces before it are kept: put these blocks at the start of their line.
  • An expression block (<%= ... %>) alone on its line keeps its line, because it writes something.
  • A block that is followed by text on the same line keeps the line break.

Blocks that are not closed

A <% without a matching %> is not an error: the rest of the template, from the <%, is written as text. If a part of your template appears in the output verbatim, look for a missing %>.

Write a delimiter as text

Write the start delimiter twice to write it once as text. The block that would start after it stays text:

using var template = new Template();
template.Load("<%<%= Name %> writes <%= 1 + 1 %>");

var result = template.Run();
Assert.Equal("<%= Name %> writes 2", result);
  • Each pair writes one delimiter: <%<%<%<% writes <%<%. A delimiter that is not doubled starts a block, so <%<%<%= 1 %> writes <%1.
  • The end delimiter needs no escape in text: %> outside a block is written as is.
  • Inside a block, the code ends at the first end delimiter, even in a string. Build the delimiter from two strings: <%= "%" + ">" %>.

The escape sequence is the start delimiter written twice, whatever the delimiters are: with {{ and }}, {{{{ writes {{:

using var template = new Template
{
    StartCodeBlockDelimiter = "{{",
    EndCodeBlockDelimiter = "}}",
};
template.Load("{{{{Name}} is {{=Name}}");
template.Arguments.Add(new TemplateArgument("Name", typeof(string)));

var result = template.Run("John");
Assert.Equal("{{Name}} is John", result);

Change the delimiters

When the default delimiters appear in the text of your template, choose other ones. Set them before calling Load:

using var template = new Template
{
    StartCodeBlockDelimiter = "{{",
    EndCodeBlockDelimiter = "}}",
};
template.Load("Hello {{=Name}}!");
template.Arguments.Add(new TemplateArgument("Name", typeof(string)));
var result = template.Run("World");
Assert.Equal("Hello World!", result);

The character that gives the kind of block (=, +, @) follows the start delimiter, whatever it is. When the text contains the delimiters only a few times, escape them instead of changing them.