ZetCode

C# Scriban

last modified October 18, 2026

In this article we show how to use the Scriban template engine in C#.

Scriban template engine

A template engine or template processor is a library designed to combine templates with a data model to produce documents. Template engines are often used to generate large amounts of emails, in source code preprocessing, or producing dynamic HTML pages.

A template consists of static parts and dynamic parts. The dynamic parts are later replaced with data. The rendering function combines the template with the data to produce the final document.

Scriban uses {{ }} for code blocks and {%{ }%} for escape blocks. Any other text is considered as a text block and is output as is. Scriban can also parse templates written in the Liquid language.

The Template.Parse method parses the text into a template and the Template.Render method renders the template into the final output using the provided data model.

$ dotnet add package Scriban

We add the Scriban package to the project.

Scriban simple example

The following is a simple Scriban example.

Program.cs
using Scriban;

var name = "John Doe";

var tpl = Template.Parse("Hello {{ name }}!");
var res = tpl.Render(new { name });

Console.WriteLine(res);

The program prints a small message to the console.

var tpl = Template.Parse("Hello {{ name }}!");

The Template.Parse method parses the specified text into a template. With {{ }} we output the content of the name variable.

var res = tpl.Render(new { name });

The Render method renders the template using the specified model. We pass an anonymous object; its name property becomes the name variable in the template.

$ dotnet run
Hello John Doe!

In the next example, we pass two variables to the template engine.

Program.cs
using Scriban;

var name = "John Doe";
var occupation = "gardener";

var tpl = Template.Parse("{{ name }} is a {{ occupation }}");
var res = tpl.Render(new { name, occupation });

Console.WriteLine(res);

The program prints a message using data from two variables: name and occupation.

$ dotnet run
John Doe is a gardener

Scriban for loop

Loops are created with the for statement.

Program.cs
using Scriban;

string[] words = ["sky", "blue", "falcon", "book", "ocean", "dog"];

var html = """
<ul>
{{- for word in words }}
    <li>{{ word }}</li>
{{- end }}
</ul>
""";

var tpl = Template.Parse(html);
var res = tpl.Render(new { words });

Console.WriteLine(res);

The example uses the for statement to output an array of words into an HTML list. The template is written as a raw string literal, which spans multiple lines and needs no escaping of quotes.

{{- for word in words }}
    <li>{{ word }}</li>
{{- end }}

We iterate over the array of words. The - character after the opening braces strips the whitespace, including the newline, on the left of the code block. Similarly, -}} strips the whitespace on the right. Without it, each code block would leave an empty line in the output.

$ dotnet run
<ul>
    <li>sky</li>
    <li>blue</li>
    <li>falcon</li>
    <li>book</li>
    <li>ocean</li>
    <li>dog</li>
</ul>

Scriban string functions

We can use several string functions in the templates. Functions are applied with the pipe operator |; the value on the left becomes the first argument of the function.

Program.cs
using Scriban;

var msg = "an old falcon";

var data = """
{{ msg | string.capitalize }}
{{ msg | string.upcase }}
The message has {{ msg | string.size }} characters.
The message has {{ msg | string.split ' ' | array.size }} words.
""";

var tpl = Template.Parse(data);
var res = tpl.Render(new { msg });

Console.WriteLine(res);

We have a string message. We apply several string functions on the message.

{{ msg | string.capitalize }}
{{ msg | string.upcase }}

We capitalize and upcase the message.

The message has {{ msg | string.size }} characters.

We count the number of characters in the message.

The message has {{ msg | string.split ' ' | array.size }} words.

We count the number of words in the message. The pipes can be chained: the result of string.split is passed to array.size.

$ dotnet run
An old falcon
AN OLD FALCON
The message has 13 characters.
The message has 3 words.

Scriban array functions

Scriban has built-in array functions.

Program.cs
using Scriban;

int[] vals = [2, 1, -3, 0, -1, -2, 3];

var data = """
The array has {{ vals.size }} elements
First element: {{ vals[0] }}
Last element: {{ vals[-1] }}
Sorted elements: {{ vals | array.sort }}
Reversed elements: {{ vals | array.reverse }}
Contains 0: {{ vals | array.contains 0 }}
Joined: {{ vals | array.join ', ' }}
""";

var tpl = Template.Parse(data);
var res = tpl.Render(new { vals });

Console.WriteLine(res);

We have an array of integers. With built-in array functions, we count, sort, reverse, search, and join the elements. A negative index counts from the end of the array.

$ dotnet run
The array has 7 elements
First element: 2
Last element: 3
Sorted elements: [-3, -2, -1, 0, 1, 2, 3]
Reversed elements: [3, -2, -1, 0, -3, 1, 2]
Contains 0: true
Joined: 2, 1, -3, 0, -1, -2, 3

Scriban conditions

We can use if/else if/else conditions in templates.

Program.cs
using Scriban;

string?[] names = ["John", "Nelly", null, "George"];

var data = """
{{- for name in names }}
{{- if !name }}
Hello there!
{{- else }}
Hello {{ name }}!
{{- end }}
{{- end }}
""";

var tpl = Template.Parse(data);
var res = tpl.Render(new { names });

Console.WriteLine(res);

We have an array of names. With the if condition, we check if the element is null. In Scriban, null and false are falsy values.

$ dotnet run
Hello John!
Hello Nelly!
Hello there!
Hello George!

Scriban read template from file

In the following example, we read the template from a file.

users.tpl
<table>
    <thead>
    <tr>
        <th>Name</th>
        <th>Occupation</th>
    </tr>
    </thead>
    <tbody>
    {{- for user in users }}
    <tr>
        <td>{{ user.name }}</td>
        <td>{{ user.occupation }}</td>
    </tr>
    {{- end }}
    </tbody>
</table>

The template file outputs data in an HTML table.

Program.cs
using Scriban;

List<User> users =
[
    new("John Doe", "gardener"),
    new("Roger Roe", "driver"),
    new("Lucy Smith", "teacher")
];

var fileName = "users.tpl";
var data = File.ReadAllText(fileName);

var tpl = Template.Parse(data, fileName);
var res = tpl.Render(new { users });

Console.WriteLine(res);

record User(string Name, string Occupation);

The data is stored in a list of records. The template file is loaded with the File.ReadAllText method.

var tpl = Template.Parse(data, fileName);

The optional second parameter is the source path; it is used in error messages.

<td>{{ user.name }}</td>

By default, Scriban exposes .NET members in snake case. The Name property of the record is accessed as name; a property such as FirstName would be accessed as first_name.

$ dotnet run
<table>
    <thead>
    <tr>
        <th>Name</th>
        <th>Occupation</th>
    </tr>
    </thead>
    <tbody>
    <tr>
        <td>John Doe</td>
        <td>gardener</td>
    </tr>
    <tr>
        <td>Roger Roe</td>
        <td>driver</td>
    </tr>
    <tr>
        <td>Lucy Smith</td>
        <td>teacher</td>
    </tr>
    </tbody>
</table>

Scriban parse errors

A template with a syntax error does not throw an exception when parsed. Instead, we check the HasErrors property and the Messages collection.

Program.cs
using Scriban;

var tpl = Template.Parse("Hello {{ name }!");

if (tpl.HasErrors)
{
    foreach (var msg in tpl.Messages)
    {
        Console.WriteLine(msg);
    }

    return;
}

Console.WriteLine(tpl.Render(new { name = "John" }));

The code block is missing one closing brace. Calling Render on such a template would throw an InvalidOperationException.

$ dotnet run
<input>(1,15) : error : Invalid token found `}`. Expecting <EOL>/end of line.
<input>(1,15) : error : Unexpected } while no matching {

Scriban custom functions

For more control over the rendering, we use a TemplateContext with a ScriptObject. A script object holds variables and functions that are available in the template.

Program.cs
using System.Globalization;
using Scriban;
using Scriban.Runtime;

var culture = CultureInfo.GetCultureInfo("en-US");

var so = new ScriptObject();
so.Add("customer", "Roger Roe");
so.Add("items", new[] { 2.5m, 10.0m, 4.25m });
so.Import("money", new Func<decimal, string>(v => v.ToString("C2", culture)));

var context = new TemplateContext();
context.PushGlobal(so);

var data = """
Customer: {{ customer }}
{{- total = 0 }}
{{- for item in items }}
  item {{ for.index + 1 }}: {{ item | money }}
  {{- total = total + item }}
{{- end }}
Total: {{ total | money }}
""";

var tpl = Template.Parse(data);
Console.WriteLine(tpl.Render(context));

The program prints a simple invoice. The prices are formatted with a custom money function.

so.Import("money", new Func<decimal, string>(v => v.ToString("C2", culture)));

The Import method registers a .NET delegate as a template function.

var context = new TemplateContext();
context.PushGlobal(so);

We push the script object to the global scope of the context and render the template with the context instead of a model object.

{{- total = 0 }}
...
  {{- total = total + item }}

Templates can define their own variables. An assignment produces no output. The for.index variable holds the zero-based index of the current iteration.

$ dotnet run
Customer: Roger Roe
  item 1: $2.50
  item 2: $10.00
  item 3: $4.25
Total: $16.75

Scriban Liquid templates

Liquid is a template language created by Shopify. Scriban can parse Liquid templates with the Template.ParseLiquid method.

Program.cs
using Scriban;

var data = """
{% for user in users -%}
{{ user.name | upcase }} is {{ user.age }} years old
{% endfor -%}
""";

var users = new[]
{
    new { name = "John Doe", age = 34 },
    new { name = "Lucy Smith", age = 28 }
};

var tpl = Template.ParseLiquid(data);
Console.WriteLine(tpl.Render(new { users }));

Liquid uses {% %} for statements and {{ }} for output. Liquid filters, such as upcase, are mapped to Scriban's built-in functions.

$ dotnet run
JOHN DOE is 34 years old
LUCY SMITH is 28 years old

Source

Scriban GitHub page

In this article we have worked with the Scriban template engine in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.