Fluid.Core
2.40.0
See the version list below for details.
dotnet add package Fluid.Core --version 2.40.0
NuGet\Install-Package Fluid.Core -Version 2.40.0
<PackageReference Include="Fluid.Core" Version="2.40.0" />
<PackageVersion Include="Fluid.Core" Version="2.40.0" />
<PackageReference Include="Fluid.Core" />
paket add Fluid.Core --version 2.40.0
#r "nuget: Fluid.Core, 2.40.0"
#:package Fluid.Core@2.40.0
#addin nuget:?package=Fluid.Core&version=2.40.0
#tool nuget:?package=Fluid.Core&version=2.40.0
<p align="center"><img width=25% src="https://github.com/sebastienros/fluid/raw/main/Assets/logo-vertical.png"></p>
Basic Overview
Fluid is an open-source .NET template engine based on the Liquid template language. It's 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 describing different stages of usages of Fluid.
<br>
Features
- Very fast Liquid parser and renderer (no-regexp), with few allocations. See benchmarks.
- Secure templates by allow-listing all the 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 tag with your own. Even with complex grammar constructs. See Customizing tags and blocks
- Parses templates in 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
- Features
- Using Fluid in your project
- NativeAOT and trimming
- Allow-listing object members
- Handling undefined variables
- Execution limits
- Converting CLR types
- Encoding
- Localization
- Money filters
- Time zones
- Customizing tags and blocks
- ASP.NET MVC View Engine
- Whitespace control
- Custom filters
- Functions
- Visiting and altering a template
- Performance
- Used by
<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.
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 declare the parser in a local static variable:
private static readonly FluidParser _parser = new FluidParser();
A IFluidTemplate instance is thread-safe and can be cached and reused by multiple threads concurrently.
A TemplateContext instance is not thread-safe and an instance should be created every time an IFluidTemplate instance is used.
<br>
NativeAOT and trimming
Fluid works when targeting NativeAOT and trimmed deployments.
- If dynamic code is not supported at runtime, Fluid automatically switches to reflection-based member accessors.
- Existing
MemberAccessStrategy.Register<T...>APIs are preserved. - No interceptor setup is required.
Recommended usage when targeting NativeAOT
- Reuse
TemplateOptionsinstances (for example, at app startup). - If you use runtime
MemberAccessStrategy.Register<T...>calls, execute them during application startup before rendering templates. - Prefer
[FluidRegister]on a customTemplateOptionssubclass for model types known at compile time. - Validate your app with AOT/trim publish settings:
dotnet publish -c Release -r <RID> -p:PublishAot=true
Source generation (optional)
When the Fluid.SourceGenerator analyzer is enabled, Fluid can generate strongly-typed member accessors for types declared with FluidRegisterAttribute.
The recommended pattern is to declare a custom TemplateOptions subclass and add one FluidRegisterAttribute per model type:
using Fluid;
[FluidRegister(typeof(Person))]
[FluidRegister(typeof(Address))]
public partial class PublicTemplateOptions : TemplateOptions
{
}
Use the generated options type like any other TemplateOptions instance:
var options = new PublicTemplateOptions();
The generated registrations are instance-scoped and are applied automatically to each PublicTemplateOptions instance. Runtime registrations still work and can be added normally:
options.MemberAccessStrategy.Register<Product, object>((product, name) => product.Name);
Alternatively, explicit profile methods can apply generated registrations to any TemplateOptions instance:
public static partial class FluidProfiles
{
[FluidRegister(typeof(Person))]
[FluidRegister(typeof(Address))]
public static partial void ApplyPublic(TemplateOptions options);
}
Use it with any options instance:
var options = new TemplateOptions();
FluidProfiles.ApplyPublic(options);
<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>
Allow-listing object members
Liquid is a secure template language which will only allow a predefined set of members to be accessed, and where model members can't be changed.
Property are added to the TemplateOptions.MemberAccessStrategy property. This options object can be reused every time a template is rendered.
Alternatively, the MemberAccessStrategy can be assigned an instance of UnsafeMemberAccessStrategy which will allow any property to be accessed.
Allow-listing a specific type
This will allow any public field or property to be read from a template.
var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Person>();
Note: When passing a model with
new TemplateContext(model)the type of themodelobject is automatically registered. This behavior can be disable by callingnew TemplateContext(model, false)
Allow-listing specific members
This will only allow the specific fields or properties to be read from a template.
var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Person>("Firstname", "Lastname");
Intercepting a type access
This will provide a method to intercept when a member is accessed and either return a custom value or prevent it.
NB: If the model implements IDictionary or any similar generic dictionary types the dictionary access has priority over the custom accessors.
This example demonstrates how to intercept calls to a Person and always return the same property.
var model = new Person { Name = "Bill" };
var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Person, object>((obj, name) => obj.Name);
Customizing object accessors
To provide advanced customization for specific types, it is recommended to use value converters and a custom FluidValue implementation by inheriting from ObjectValueBase.
The following example show how to provide a custom transformation for any Person object:
private class PersonValue : ObjectValueBase
{
public PersonValue(Person value) : base(value)
{
}
public override ValueTask<FluidValue> GetIndexAsync(FluidValue index, TemplateContext context)
{
return Create(((Person)Value).Firstname + "!!!" + index.ToStringValue(), context.Options);
}
}
This custom type can be used with a converter such 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);
It can also be used to replace custom member access by customizing GetValueAsync, or do custom conversions to standard Fluid types.
<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 as a string parameter.
var missingVariables = new List<string>();
var context = new TemplateContext();
context.Undefined = name =>
{
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' | upcase }} => HELLO
Use StrictFilters together with StrictVariables to enforce both variable and filter correctness during authoring.
Returning custom values for undefined values
The Undefined delegate can return a custom FluidValue to provide fallback values or error messages for missing values:
var options = new TemplateOptions
{
Undefined = name =>
{
// Return a custom default value for undefined variables
return ValueTask.FromResult<FluidValue>(new StringValue($"[{name} not found]"));
}
};
var template = FluidTemplate.Parse("Hello {{ user.name }} in {{ city }}!");
var context = new TemplateContext(options);
var result = await template.RenderAsync(context);
// Outputs: "Hello [user.name not found] in [city not found]!"
Logging undefined accesses
You can use the Undefined delegate to log missing values for debugging or monitoring:
var options = new TemplateOptions
{
Undefined = path =>
{
Console.WriteLine($"Missing variable: {path}");
return ValueTask.FromResult<FluidValue>(NilValue.Instance);
}
};
var template = FluidTemplate.Parse("{{ first }} {{ second }}");
var context = new TemplateContext(options);
await template.RenderAsync(context);
// Logs: "Missing variable: first"
// Logs: "Missing variable: second"
<br>
Inheritance
All the members of the class hierarchy are registered. Besides, all inherited classes will be correctly evaluated when a base class is registered and a member of the base class is accessed.
<br>
Object members casing
By default, the properties of a registered object are case sensitive and registered as they are in their source code. For instance,
the property FirstName would be access using the {{ p.FirstName }} tag.
However it can be necessary to register these properties with different cases, like Camel case (firstName), or Snake case (first_name).
The following example configures the templates to use Camel casing.
var options = new TemplateOptions();
options.MemberAccessStrategy.MemberNameStrategy = MemberNameStrategies.CamelCase;
Execution limits
Limiting templates recursion
When invoking {% include 'sub-template' %} statements it is possible that some templates create an infinite recursion that could block the server.
To prevent this the TemplateOptions class defines a default MaxRecursion = 100 that prevents templates from being have a depth greater than 100.
Limiting templates execution
Template can inadvertently create infinite loop that could block the server by running indefinitely.
To prevent this the TemplateOptions class defines a default MaxSteps. By default this value is not set.
<br>
Converting CLR types
Whenever an object is manipulated in a template it is converted to a specific FluidValue instance that provides a dynamic type system somehow similar to the one in JavaScript.
In Liquid they can be Number, String, Boolean, Array, Dictionary, or Object. Fluid will automatically convert the CLR types to the corresponding Liquid ones, and also provides specialized ones.
To be able to customize this conversion you can add value converters.
Adding a value converter
When the conversion logic is not directly inferred from the type of an object, a value converter can be used.
Value converters can return:
nullto indicate that the value couldn't be converted- a
FluidValueinstance to stop any further conversion and use this value - another object instance to continue the conversion using custom and internal type mappings
The following example shows how to convert any instance implementing an interface to a custom string value:
var options = new TemplateOptions();
options.ValueConverters.Add((value) => value is IUser user ? user.Name : null);
Note: Type mapping are defined globally for the application.
<br>
Encoding
By default Fluid doesn't encode the output. Encoders can be specified when calling Render() or RenderAsync() on the template.