Fluid.Core 2.2.15

.NET 5.0 .NET Core 3.1 .NET Standard 2.0
Install-Package Fluid.Core -Version 2.2.15
dotnet add package Fluid.Core --version 2.2.15
<PackageReference Include="Fluid.Core" Version="2.2.15" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Fluid.Core --version 2.2.15
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Fluid.Core, 2.2.15"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install Fluid.Core as a Cake Addin
#addin nuget:?package=Fluid.Core&version=2.2.15

// Install Fluid.Core as a Cake Tool
#tool nuget:?package=Fluid.Core&version=2.2.15
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

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

NuGet MIT

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>

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

<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>

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 the model object is automatically registered. This behavior can be disable by calling new 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.

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:

  • null to indicate that the value couldn't be converted
  • a FluidValue instance 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.

HTML encoding

To render a template with HTML encoding use the System.Text.Encodings.Web.HtmlEncoder.Default instance.

This encoder is used by default for the MVC View engine.

Disabling encoding contextually

When an encoder is defined you can use a special raw filter or {% raw %} ... {% endraw %} tag to prevent a value from being encoded, for instance if you know that the content is HTML and is safe.

Source
{% assign html = '<em>This is some html</em>' %}

Encoded: {{ html }}
Not encoded: {{ html | raw }
Result
&lt;em%gt;This is some html&lt;/em%gt;
<em>This is some html</em>

Captured blocks are not double-encoded

When using capture blocks, the inner content is flagged as pre-encoded and won't be double-encoded if used in a {{ }} tag.

Source
{% capture breaktag %}<br />{% endcapture %}

{{ breaktag }}
Result
<br />

<br>

Localization

By default templates are rendered using an invariant culture so that the results are consistent across systems. This is important for instance when rendering dates, times and numbers.

However it is possible to define a specific culture to use when rendering a template using the TemplateContext.CultureInfo property.

Source
var options = new TemplateOptions();
options.CultureInfo = new CultureInfo("en-US");
var context = new TemplateContext(options);
var result = template.Render(context);
{{ 1234.56 }}
{{ "now" | date: "%v" }}
Result
1234.56
Tuesday, August 1, 2017

<br>

Time zones

System time zone

TemplateOptions and TemplateContext provides a property to define a default time zone to use when parsing date and times. The default value is the current system's time zone. When dates and times are parsed and don't specify a time zone, the default one is assumed. Setting a custom one can also prevent different environments (data centers) from generating different results.

Note: The date filter conforms to the Ruby date and time formats https://ruby-doc.org/core-3.0.0/Time.html#method-i-strftime. To use the .NET standard date formats, use the format_date filter.

Source
var context = new TemplateContext { TimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") } ;
var result = template.Render(context);
{{ '1970-01-01 00:00:00' | date: '%c' }}
Result
Wed Dec 31 19:00:00 -08:00 1969

Converting time zones

Dates and times can be converted to specific time zones using the time_zone: <iana> filter.

Example
var context = new TemplateContext();
context.SetValue("published", DateTime.UtcNow);
{{ published | time_zone: 'America/New_York' | date: '%+' }}
Result
Tue Aug  1 17:04:36 -05:00 2017

<br>

Customizing tags and blocks

Fluid's grammar can be modified to accept any new tags and blocks with any custom parameters. The parser is based on Parlot which makes it completely extensible.

Unlike blocks, tags don't have a closing element (e.g., cycle, increment). A closing element will match the name of the opening tag with and end suffix, like endfor. Blocks are useful when manipulating a section of a a template as a set of statements.

Fluid provides helper method to register common tags and blocks. All tags and block always start with an identifier that is the tag name.

Each custom tag needs to provide a delegate that is evaluated when the tag is matched. Each degate will be able to use these properties:

  • writer, a TextWriter instance that is used to render some text.
  • encode, a TextEncoder instance, like HtmlEncoder, or NullEncoder. It's defined by the caller of the template.
  • context, a TemplateContext instance.

Registering a custom tag

  • Empty: Tag with no parameter, like {% renderbody %}
  • Identifier: Tag taking an identifier as parameter, like {% increment my_variable %}
  • Expression: Tag taking an expression as parameter, like {% layout 'home' | append: '.liquid' %}

Here are some examples:

Source
parser.RegisterIdentifierTag("hello", (identifier, writer, encoder, context) =>
{
    writer.Write("Hello ");
    writer.Write(identifier);
});
{% hello you %}
Result
Hello you

Registering a custom block

Blocks are created the same way as tags, and the lambda expression can then access the list of statements inside the block.

Source

parser.RegisterExpressionBlock("repeat", (value, statements, writer, encoder, context) =>
{
    for (var i = 0; i < value.ToNumber(); i++)
    {
      await return statements.RenderStatementsAsync(writer, encoder, context);
    }

    return Completion.Normal;
});
{% repeat 1 | plus: 2 %}Hi! {% endrepeat %}
Result
Hi! Hi! Hi!

Custom parsers

If identifier, empty and expression parsers are not sufficient, the methods RegisterParserBlock and RegisterParserTag accept any custom parser construct. These can be the standard ones defined in the FluidParser class, like Primary, or any other composition of them.

For instance, RegisterParseTag(Primary.AndSkip(Comma).And(Primary), ...) will expect two Primary elements separated by a comma. The delegate will then be invoked with a ValueTuple<Expression, Expression> representing the two Primary expressions.

Registering a custom operator

Operator are used to compare values, like > or contains. Custom operators can be defined if special comparisons need to be provided.

Source

The following example creates a custom xor operator that will evaluate to true if only one of the left and right expressions is true when converted to booleans.

XorBinaryExpression.cs

using Fluid.Ast;
using Fluid.Values;
using System.Threading.Tasks;

namespace Fluid.Tests.Extensibility
{
    public class XorBinaryExpression : BinaryExpression
    {
        public XorBinaryExpression(Expression left, Expression right) : base(left, right)
        {
        }

        public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext context)
        {
            var leftValue = await Left.EvaluateAsync(context);
            var rightValue = await Right.EvaluateAsync(context);

            return BooleanValue.Create(leftValue.ToBooleanValue() ^ rightValue.ToBooleanValue());
        }
    }
}

Parser configuration

parser.RegisteredOperators["xor"] = (a, b) => new XorBinaryExpression(a, b);

Usage

{% if true xor false %}Hello{% endif %}
Result
Hello

<br>

ASP.NET MVC View Engine

The package Fluid.MvcViewEngine provides a convenient way to use Liquid as a replacement or in combination of Razor in ASP.NET MVC.

Configuration

Registering the view engine
  1. Reference the Fluid.MvcViewEngine NuGet package
  2. Add a using statement on Fluid.MvcViewEngine
  3. Call AddFluid() in your Startup.cs.
Sample
using FluidMvcViewEngine;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddFluid();
    }
}
Registering view models

Because the Liquid language only accepts known members to be accessed, the View Model classes need to be registered in Fluid. Usually from a static constructor such that the code is run only once for the application.

View Model registration

View models are automatically registered and available as the root object in liquid templates. Custom model regsitrations can be added when calling AddFluid().

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().AddFluid(o => o.TemplateOptions.Register<Person>());
    }
}

More way to register types and members can be found in the Allow-listing object members section.

Registering custom tags

When using the MVC View engine, custom tags can still be added to the parser. Refer to this section on how to create custom tags.

It is recommended to create a custom class inheriting from FluidViewParser, and to customize the tags in the constructor of this new class. This class can then be registered as the default parser for the MVC view engine.

using Fluid.Ast;
using Fluid.MvcViewEngine;

namespace Fluid.MvcSample
{
    public class CustomFluidViewParser : FluidViewParser
    {
        public CustomFluidViewParser()
        {
            RegisterEmptyTag("mytag", static async (s, w, e, c) =>
            {
                await w.WriteAsync("Hello from MyTag");

                return Completion.Normal;
            });
        }
    }
}
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<MvcViewOptions>(options =>
        {
            options.Parser = new CustomFluidViewParser();
        });

        services.AddMvc().AddFluid();
    }
}

Layouts

Index.liquid
{% layout '_layout.liquid' %}

This is the home page

The {% layout [template] %} tag accepts one argument which can be any expression that return the relative location of a liquid template that will be used as the master template.

The layout tag is optional in a view. It can also be defined multiple times or conditionally.

From a layout template the {% renderbody %} tag is used to depict the location of the view's content inside the layout itself.

Layout.liquid
<html>
  <body>
    <div class="menu"></div>
    
    <div class="content">
      {% renderbody %}
    </div>
    
    <div class="footer"></div>
  </body>
</html>

Sections

Sections are defined in a layout as for views to render content in specific locations. For instance a view can render some content in a menu or a footer section.

Rendering content in a section
{% layout '_layout.liquid' %}

This is is the home page

{% section menu %}
  <a href="h#">This link goes in the menu</a>
{% endsection %}

{% section footer %}
  This text will go in the footer
{% endsection %}
Rendering the content of a section
<html>
  <body>
    <div class="menu">
      {% rendersection menu %}
    </div>
    
    <div class="content">
      {% renderbody %}
    </div>
    
    <div class="footer">
      {% rendersection footer %}
    </div>
  </body>
</html>

ViewStart files

Defining the layout template in each view might me cumbersome and make it difficult to change it globally. To prevent that it can be defined in a _ViewStart.liquid file.

When a view is rendered all _ViewStart.liquid files from its current and parent directories are executed before. This means multiple files can be defined to defined settings for a group of views.

_ViewStart.liquid
{% layout '_layout.liquid' %}
{% assign background = 'ffffff' }

You can also define other variables or render some content.

Custom views locations

It is possible to add custom file locations containing views by adding them to FluidMvcViewOptions.ViewsLocationFormats.

The default ones are:

  • Views/{1}/{0}.liquid
  • Views/Shared/{0}.liquid

Where {0} is the view name, and {1} is the controller name.

For partials, the list is defined in FluidMvcViewOptions.PartialsLocationFormats:

  • Views/{0}.liquid
  • Views/Partials/{0}.liquid
  • Views/Partials/{1}/{0}.liquid
  • Views/Shared/Partials/{0}.liquid

Layouts will be searched in the same locations as Views.

Execution

The content of a view is parsed once and kept in memory until the file or one of its dependencies changes. Once parsed, the tag are executed every time the view is called. To compare this with Razor, where views are first compiled then instantiated every time they are rendered. This means that on startup or when the view is changed, views with Fluid will run faster than those in Razor, unless you are using precompiled Razor views. In all cases Razor views will be faster on subsequent calls as they are compiled directly to C#.

This difference makes Fluid very adapted for rapid development cycles where the views can be deployed and updated frequently. And because the Liquid language is secure, developers give access to them with more confidence.

<br>

View Engine

The Fluid ASP.NET MVC View Engine is based on an MVC agnostic view engine provided in the Fluid.ViewEngine package. The same options and features are available, but without requiring ASP.NET MVC. This is useful to provide the same experience to build template using layouts and sections.

Usage

Use the class FluidViewRenderer : IFluidViewRender and FluidViewEngineOptions.

Whitespace control

Liquid follows strict rules with regards to whitespace support. By default all spaces and new lines are preserved from the template. The Liquid syntax and some Fluid options allow to customize this behavior.

Hyphens

For example:

{%  assign name = "Bill" %}
{{ name }}

There is a new line after the assign tag which will be preserved.

Outputs:


Bill

Tags and values can use hyphens to strip whitespace.

Example:

{%  assign name = "Bill" -%}
{{ name }}

Outputs:

Bill

The -%} strips the whitespace from the right side of the assign tag.

Template Options

Fluid provides the TemplateOptions.Trimming property that can be set with predefined preferences for when whitespace should be stripped automatically, even if hyphens are not present in tags and output values.

Greedy Mode

When greedy model is disabled in TemplateOptions.Greedy, only the spaces before the first new line are stripped. Greedy mode is enabled by default since this is the standard behavior of the Liquid language.

<br>

Custom filters

Some non-standard filters are provided by default

format_date

Formats date and times using standard .NET date and time formats. It uses the current culture of the system.

Input

"now" | format_date: "G"

Output

6/15/2009 1:45:30 PM

Documentation: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings

format_number

Formats numbers using standard .NET number formats.

Input

123 | format_number: "N"

Output

123.00

Documentation: https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-numeric-format-strings

format_string

Formats custom string using standard .NET format strings.

Input

"hello {0} {1:C}" | format_string: "world" 123

Output

hello world $123.00

Documentation: https://docs.microsoft.com/en-us/dotnet/api/system.string.format

<br>

Functions

Fluid provides optional support for functions, which is not part of the standard Liquid templating language. As such it is not enabled by default.

Enabling functions

When instantiating a FluidParser set the FluidParserOptions.AllowFunction property to true.

var parser = new FluidParser(new FluidParserOptions { AllowFunctions = true });

When functions are used while the feature is not enabled, a parse error will be returned.

Declaring local functions with the macro tag

macro allows you to define reusable chunks of content invoke with local function.

{% macro field(name, value='', type='text') %}
<div class="field">
  <input type="{{ type }}" name="{{ name }}"
         value="{{ value }}" />
</div>
{% endmacro %}

Now field is available as a local property of the template and can be invoked as a function.

{{ field('user') }}
{{ field('pass', type='password') }}

Macros need to be defined before they are used as they are discovered as the template is executed. They can also be defined in external templates and imported using the {% include %} tag.

Extensibility

Functions are FluidValue instances implementing the InvokeAsync method. It allows any template to be provided custom function values as part of the model, the TemplateContext or globally with options.

A FunctionValue type is also available to provide out of the box functions. It takes a delegate that returns a ValueTask<FluidValue> as the result.

var lowercase = new FunctionValue((args, context) => 
{
  var firstArg = args.At(0).ToStringValue();
  var lower = firstArg.ToLowerCase();
  return new ValueTask<FluidValue>(new StringValue(lower));
});

var context = new TemplateContext();
context.SetValue("tolower", lowercase);

var parser = new FluidParser(new FluidParserOptions { AllowFunctions = true });
parser.TryParse("{{ tolower('HELLO') }}", out var template, out var error);
template.Render(context);

<br>

Performance

Caching

Some performance boost can be gained in your application if you decide to cache the parsed templates before they are rendered. Even though parsing is memory-safe as it won't induce any compilation (meaning all the memory can be collected if you decide to parse a lot of templates), you can skip the parsing step by storing and reusing the FluidTemplate instance.

These object are thread-safe as long as each call to Render() uses a dedicated TemplateContext instance.

Benchmarks

A benchmark application is provided in the source code to compare Fluid, Scriban, DotLiquid, Liquid.NET and Handlebars.NET (https://github.com/Handlebars-Net). Run it locally to analyze the time it takes to execute specific templates.

Results

Fluid is faster and allocates less memory than all other well-known .NET Liquid parsers. For parsing, Fluid is 30% faster than Scriban, allocating 2 times less memory. For rendering, Fluid is slightly faster than Handlebars, 3 times faster than Scriban, but allocates at least half the memory. Compared to DotLiquid, Fluid renders 9 times faster, and allocates 30 times less memory.

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.22000
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=6.0.100
  [Host]     : .NET Core 6.0.0 (CoreCLR 6.0.21.52210, CoreFX 6.0.21.52210), X64 RyuJIT
  DefaultJob : .NET Core 6.0.0 (CoreCLR 6.0.21.52210, CoreFX 6.0.21.52210), X64 RyuJIT


|             Method |          Mean |       Error |      StdDev |  Ratio | RatioSD |     Gen 0 |    Gen 1 |   Gen 2 |   Allocated |
|------------------- |--------------:|------------:|------------:|-------:|--------:|----------:|---------:|--------:|------------:|
|        Fluid_Parse |      5.813 us |   0.0230 us |   0.0204 us |   1.00 |    0.00 |    0.4196 |        - |       - |      2.6 KB |
|      Scriban_Parse |      7.851 us |   0.0545 us |   0.0510 us |   1.35 |    0.01 |    1.1902 |   0.0458 |       - |     7.32 KB |
|    DotLiquid_Parse |     18.535 us |   0.3078 us |   0.2879 us |   3.19 |    0.05 |    2.6245 |   0.0305 |       - |    16.21 KB |
|    LiquidNet_Parse |     70.425 us |   0.6556 us |   0.5812 us |  12.12 |    0.12 |   10.1318 |   0.9766 |       - |    62.08 KB |
|   Handlebars_Parse |  3,313.126 us |  18.5763 us |  16.4674 us | 569.95 |    3.62 |   23.4375 |  11.7188 |       - |   158.32 KB |
|                    |               |             |             |        |         |           |          |         |             |
|     Fluid_ParseBig |     31.433 us |   0.2956 us |   0.2621 us |   1.00 |    0.00 |    1.8311 |   0.0610 |       - |    11.53 KB |
|   Scriban_ParseBig |     42.889 us |   0.1736 us |   0.1450 us |   1.36 |    0.02 |    5.4321 |   0.7324 |       - |    33.53 KB |
| DotLiquid_ParseBig |     68.809 us |   0.6813 us |   0.6373 us |   2.19 |    0.03 |   15.3809 |   1.0986 |       - |    94.36 KB |
| LiquidNet_ParseBig | 23,711.643 us | 241.8211 us | 226.1996 us | 754.77 |    8.40 | 4656.2500 |  93.7500 |       - | 28543.66 KB |
|                    |               |             |             |        |         |           |          |         |             |
|       Fluid_Render |    303.708 us |   2.2888 us |   2.1409 us |   1.00 |    0.00 |   15.1367 |   3.4180 |       - |    95.65 KB |
|     Scriban_Render |    974.136 us |   4.7278 us |   3.9479 us |   3.21 |    0.03 |   66.4063 |  66.4063 | 66.4063 |   486.59 KB |
|   DotLiquid_Render |  2,738.586 us |  22.8373 us |  21.3621 us |   9.02 |    0.11 |  539.0625 |  93.7500 | 27.3438 |  3363.97 KB |
|   LiquidNet_Render |  1,726.024 us |  18.2744 us |  17.0938 us |   5.68 |    0.06 |  511.7188 | 234.3750 |       - |  3144.26 KB |
|  Handlebars_Render |    336.993 us |   2.0607 us |   1.8268 us |   1.11 |    0.01 |   31.7383 |   7.8125 |       - |   195.11 KB |

Tested on 2/1/2022 with

  • Scriban 5.0.0
  • DotLiquid 2.2.548
  • Liquid.NET 0.10.0
  • Handlebars.Net 2.0.9
Legend
  • Parse: Parses a simple HTML template containing filters and properties
  • ParseBig: Parses a Blog Post template.
  • Render: Renders a simple HTML template containing filters and properties, with 500 products.

Used by

Fluid is known to be used in the following projects:

Please file an issue to be listed here.

Product Versions
.NET net5.0 net5.0-windows net6.0 net6.0-android net6.0-ios net6.0-maccatalyst net6.0-macos net6.0-tvos net6.0-windows
.NET Core netcoreapp2.0 netcoreapp2.1 netcoreapp2.2 netcoreapp3.0 netcoreapp3.1
.NET Standard netstandard2.0 netstandard2.1
.NET Framework net461 net462 net463 net47 net471 net472 net48
MonoAndroid monoandroid
MonoMac monomac
MonoTouch monotouch
Tizen tizen40 tizen60
Xamarin.iOS xamarinios
Xamarin.Mac xamarinmac
Xamarin.TVOS xamarintvos
Xamarin.WatchOS xamarinwatchos
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (28)

Showing the top 5 NuGet packages that depend on Fluid.Core:

Package Downloads
NJsonSchema.CodeGeneration

JSON Schema reader, generator and validator for .NET

Elsa.Scripting.Liquid

Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application. This package provides a Liquid expression evaluator based on Fluid.

OrchardCore.Liquid.Abstractions The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org.

Orchard Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. Abstractions for liquid syntax.

FluentEmail.Liquid

Generate emails using Liquid templates. Uses the Fluid project under the hood.

DurableFunctionsMonitor.DotNetBackend

DurableFunctionsMonitor.DotNetBackend

GitHub repositories (13)

Showing the top 5 popular GitHub repositories that depend on Fluid.Core:

Repository Stars
OrchardCMS/OrchardCore
Orchard Core is an open-source modular and multi-tenant application framework built with ASP.NET Core, and a content management system (CMS) built on top of that framework.
elsa-workflows/elsa-core
A .NET Standard 2.0 Workflows Library
SeriaWei/ZKEACMS
ZKEACMS build with .Net 5 (.Net CMS)可视化设计在线编辑内容管理系统
lukencode/FluentEmail
All in one email sender for .NET. Supports popular senders (SendGrid, MailGun, etc) and Razor templates.
scriban/scriban
A fast, powerful, safe and lightweight scripting language and engine for .NET
Version Downloads Last updated
2.2.15 92,990 4/12/2022
2.2.14 523,649 1/13/2022
2.2.13 3,878 1/8/2022
2.2.12 270 1/7/2022
2.2.11 183 1/7/2022
2.2.10 291 1/7/2022
2.2.9 899 1/5/2022
2.2.8 284,891 12/9/2021
2.2.7 36,508 12/2/2021
2.2.6 833 12/1/2021
2.2.5 2,885 11/27/2021
2.2.4 5,484 11/25/2021
2.2.3 3,055 11/22/2021
2.2.2 1,198 11/20/2021
2.2.1 1,019 11/20/2021
2.2.0 13,083 11/2/2021
2.1.4 11,035 10/26/2021
2.1.3 8,075 10/19/2021
2.1.2 2,990 10/16/2021
2.1.1 573 10/15/2021
2.1.0 33,020 10/12/2021
2.0.13 223,047 5/29/2021
2.0.12 4,622 5/25/2021
2.0.11 72,205 5/20/2021
2.0.10 25,510 5/8/2021
2.0.9 9,561 5/4/2021
2.0.8 1,382 5/3/2021
2.0.7 4,969 4/23/2021
2.0.6 6,847 4/21/2021
2.0.5 8,432 4/14/2021
2.0.4 380 4/14/2021
2.0.3 3,446 4/10/2021
2.0.2 4,304 4/9/2021
2.0.1 6,756 4/6/2021
2.0.0-beta-1014 1,465 4/2/2021
2.0.0-beta-1013 1,150 3/29/2021
2.0.0-beta-1012 784 3/27/2021
2.0.0-beta-1011 203 3/25/2021
2.0.0-beta-1010 9,389 3/15/2021
2.0.0-beta-1009 1,822 3/12/2021
2.0.0-beta-1008 389 3/9/2021
2.0.0-beta-1007 2,688 2/28/2021
2.0.0-beta-1006 213 2/25/2021
2.0.0-beta-1005 189 2/25/2021
2.0.0-beta-1004 546 2/10/2021
2.0.0-beta-1003 1,374 1/25/2021
2.0.0-beta-1002 249 1/22/2021
2.0.0-beta-1001 186,836 1/19/2021
1.0.0 177,958 1/18/2021
1.0.0-beta-9722 37,802 12/4/2020
1.0.0-beta-9693 113,969 9/21/2020
1.0.0-beta-9681 12,615 8/6/2020
1.0.0-beta-9678 2,927 7/21/2020
1.0.0-beta-9672 12,234 6/22/2020
1.0.0-beta-9663 2,276 6/8/2020
1.0.0-beta-9660 3,380 6/8/2020
1.0.0-beta-9651 133,299 5/21/2020
1.0.0-beta-9637 39,898 3/2/2020
1.0.0-beta-9634 9,356 1/15/2020
1.0.0-beta-9626 8,596 1/8/2020
1.0.0-beta-9624 1,775 12/28/2019
1.0.0-beta-9621 434 12/28/2019
1.0.0-beta-9619 449 12/28/2019
1.0.0-beta-9617 505 12/24/2019
1.0.0-beta-9614 472 12/24/2019
1.0.0-beta-9608 50,577 10/30/2019
1.0.0-beta-9605 99,919 10/10/2019
1.0.0-beta-9599 68,557 9/20/2019
1.0.0-beta-9588 9,006 8/28/2019
1.0.0-beta-9586 1,282 8/22/2019
1.0.0-beta-9578 4,536 8/15/2019
1.0.0-beta-9571 65,960 4/2/2019
1.0.0-beta-9560 2,397 3/22/2019
1.0.0-beta-9558 683 3/21/2019
1.0.0-beta-9554 1,613 3/14/2019
1.0.0-beta-9545 31,750 3/5/2019
1.0.0-beta-9542 563 3/3/2019
1.0.0-beta-9519 16,085 2/4/2019
1.0.0-beta-9513 3,859 1/22/2019
1.0.0-beta-9507 721 1/21/2019
1.0.0-beta-9504 1,025 1/17/2019
1.0.0-beta-9501 590 1/17/2019
1.0.0-beta-9480 11,098 12/4/2018
1.0.0-beta-9463 3,176 11/15/2018
1.0.0-beta-9446 14,796 6/22/2018
1.0.0-beta-9442 24,436 6/22/2018
1.0.0-beta-9422 64,770 1/22/2018
1.0.0-beta-9415 1,293 1/16/2018
1.0.0-beta-9399 78,634 11/5/2017
1.0.0-beta-9398 758 11/5/2017
1.0.0-beta-9389 1,060 11/2/2017
1.0.0-beta-9334 1,455 8/30/2017
1.0.0-beta-9333 782 8/30/2017
1.0.0-beta-9327 6,362 8/15/2017
1.0.0-beta-9325 789 8/13/2017
1.0.0-beta-9308 878 8/10/2017
1.0.0-beta-9300 1,632 8/2/2017
1.0.0-beta-93 818 7/31/2017
1.0.0-beta-84 988 6/30/2017
1.0.0-beta-67 1,006 6/20/2017
1.0.0-beta-47 731 6/16/2017
1.0.0-beta-32 5,286 6/15/2017
1.0.0-beta-30 714 6/15/2017
1.0.0-beta-21 728 6/14/2017
1.0.0-beta-20 721 6/14/2017
1.0.0-beta-18 746 6/14/2017
1.0.0-beta-107 786 8/2/2017
0.1.0-alpha-11 772 6/13/2017