SkillAgentSearch skills...

Fluid

Fluid is an open-source .NET template engine based on the Liquid template language.

Install / Use

/learn @sebastienros/Fluid
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<p align="center"><img width=25% src="https://github.com/sebastienros/fluid/raw/main/Assets/logo-vertical.png"></p>

NuGet MIT MyGet

Basic Overview

Fluid is an open-source .NET template engine based on the Liquid template language. It is a secure template language that is also very accessible for non-programmer audiences.

The following content is based on the 2.0.0-beta version, which is the recommended version, even though some of its API might vary significantly. To see the corresponding content for v1.0, use this version

<br>

Tutorials

Deane Barker wrote a very comprehensive tutorial on how to write Liquid templates with Fluid. For a high-level overview, read The Four Levels of Fluid Development, which describes different stages of using Fluid.

<br>

Features

  • Very fast Liquid parser and renderer (no-regexp), with few allocations. See benchmarks.
  • Secure templates by allow-listing all available properties in the template. User templates can't break your application.
  • Supports async filters. Templates can execute database queries more efficiently under load.
  • Customize filters and tags with your own, even with complex grammar constructs. See Customizing tags and blocks.
  • Parses templates into a concrete syntax tree that lets you cache, analyze, and alter the templates before they are rendered.
  • Register any .NET types and properties, or define custom handlers to intercept when a named variable is accessed.
<br>

Contents

<br>

Source

<ul id="products">
  {% for product in products %}
    <li>
      <h2>{{product.name}}</h2>
      Only {{product.price | price }}

      {{product.description | prettyprint | paragraph }}
    </li>
  {% endfor %}
</ul>

Result

<ul id="products">
    <li>
      <h2>Apple</h2>
      $329

      Flat-out fun.
    </li>
    <li>
      <h2>Orange</h2>
      $25

      Colorful. 
    </li>
    <li>
      <h2>Banana</h2>
      $99

      Peel it.
    </li>
</ul>

Notice

  • The <li> tags are at the same index as in the template, even though the {% for } tag had some leading spaces
  • The <ul> and <li> tags are on contiguous lines even though the {% for } is taking a full line.
<br>

Using Fluid in your project

You can directly reference the NuGet package.

The code samples in this document assume you have registered the Fluid namespace with using Fluid;.

Hello World

Source

var parser = new FluidParser();

var model = new { Firstname = "Bill", Lastname = "Gates" };
var source = "Hello {{ Firstname }} {{ Lastname }}";

if (parser.TryParse(source, out var template, out var error))
{   
    var context = new TemplateContext(model);

    Console.WriteLine(template.Render(context));
}
else
{
    Console.WriteLine($"Error: {error}");
}

Result

Hello Bill Gates

Thread-safety

A FluidParser instance is thread-safe and should be shared by the whole application. A common pattern is to declare the parser in a local static variable:

    private static readonly FluidParser _parser = new FluidParser();

An IFluidTemplate instance is thread-safe and can be cached and reused by multiple threads concurrently.

A TemplateContext instance is not thread-safe, and a new instance should be created every time an IFluidTemplate instance is used.

<br>

Adding custom filters

Filters can be async or not. They are defined as a delegate that accepts an input, a set of arguments and the current context of the rendering process.

Here is the downcase filter as defined in Fluid.

Source

public static ValueTask<FluidValue> Downcase(FluidValue input, FilterArguments arguments, TemplateContext context)
{
    return new StringValue(input.ToStringValue().ToLower());
}

Registration

Filters are registered in an instance of TemplateOptions. This options object can be reused every time a template is rendered.

var options = new TemplateOptions();
options.Filters.AddFilter('downcase', Downcase);

var context = new TemplateContext(options);
<br>

Altering exposed .NET properties

Converting object types

Use the ValueConverters property to return different values than those provided by the model classes and properties:

var options = new TemplateOptions();
options.ValueConverters.Add(o => o is DateTime d ? new StringValue($"This is a date time: {d}") : null);

The previous example will return a custom value instead of the actual DateTime. When no conversion should be applied, null is returned.

Customizing object properties

A common scenario is to access named properties on an object that do not exist in the source class, or should return a different result.

In this case, the ValueConverters can be used to return a specific wrapper/proxy FluidValue instance. In practice, you can inherit from ObjectValueBase as it implements how most objects should behave.

The following example shows how to provide a custom transformation for any Person object:

private class PersonValue : ObjectValueBase
{
    public PersonValue(Person value) : base(value)
    {
    }

    public override ValueTask<FluidValue> GetValueAsync(string name, TemplateContext context)
    {
        if (name == "Bingo")
        {
          return new StringValue("Hello, World!");
        }
    }
}

This custom type can be used with a converter so that any time a Person is used, it is wrapped as a PersonValue.

var options = new TemplateOptions();
options.ValueConverters.Add(o => o is Person p ? new PersonValue(p) : null);

Invoking the member Bingo on a Person instance will then return the string "Hello, World!":

{{ myPerson.Bingo }}

Note: This technique can also be used to substitute existing properties with other values or even computed data.

<br>

Handling undefined values

Fluid evaluates members lazily, so undefined identifiers can be detected precisely when they are consumed. By default, undefined values render as empty strings without raising errors.

Tracking undefined values

To track missing values during template rendering, assign a delegate to TemplateOptions.Undefined or TemplateContext.Undefined. This delegate is called each time an undefined variable is accessed and receives the variable path and parent object type. The type argument is null when there is no target object (for example, when a global value is missing).

var missingVariables = new List<string>();

var context = new TemplateContext();
context.Undefined = (name, type) =>
    {
        missingVariables.Add(name);
        return ValueTask.FromResult<FluidValue>(NilValue.Instance);
    }
};

var template = FluidTemplate.Parse("Hello {{ user.name }} in {{ city }}!");

await template.RenderAsync(context);

### Strict variables

If you prefer templates to fail fast when they reference a variable that does not exist, enable strict variable mode by setting `TemplateOptions.StrictVariables` to `true`. When `StrictVariables` is `true`, any attempt to access an undefined variable throws a `FluidException` containing the variable name. This makes missing data issues visible immediately instead of silently rendering as an empty string.

```csharp
var options = new TemplateOptions { StrictVariables = true };
var context = new TemplateContext(options);

// Parsing a template that references an undefined variable
var template = FluidTemplate.Parse("Hello {{ user.name }}!");

// Throws FluidException: Undefined variable 'user'
await template.RenderAsync(context);

When StrictVariables is disabled (the default), you can still track missing variables using the Undefined delegate described above, or provide fallback values by returning a custom FluidValue.

// missingVariables now contains ["user.name", "city"]


### Strict filters

By default, applying an unknown filter simply returns the input value unchanged:

```liquid
{{ 'hello' | unknown }}  => hello

If you would rather fail fast when a template references a filter that has not been registered, enable strict filter mode by setting TemplateOptions.StrictFilters to true:

var options = new TemplateOptions { StrictFilters = true };
var context = new TemplateContext(options);

var template = FluidTemplate.Parse("{{ 'hello' | unknown }}");
// Throws FluidException: Undefined filter 'unknown'
await template.RenderAsync(context);

Known filters continue to work normally when StrictFilters is enabled:

{{ 'hello'
View on GitHub
GitHub Stars1.7k
CategoryDevelopment
Updated9h ago
Forks191

Languages

C#

Security Score

100/100

Audited on Mar 24, 2026

No findings