Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
F

fluid

> 编程语言
Open source

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

1.8K stars0 likes1 views
WebsiteGitHub

About

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

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

This document describes Fluid 3.0, which is under development on main. Preview packages are available from the preview feed; stable releases are available on NuGet.org. To see the corresponding content for v1.0, use this version


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.


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.

Contents

  • Features
  • Using Fluid in your project
  • Preview packages
  • NativeAOT and trimming
  • Source generator
  • 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

Source

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

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

Result

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

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

Preview packages

After a successful build triggered by a push to main, preview packages are published to the Fluid feed on feedz.io. Versions follow 3.0.0-preview-<run number>, using the GitHub Actions build run number. These packages contain the latest development changes and are intended for testing before release. Tagged releases continue to be published to NuGet.org.

Add the preview feed alongside NuGet.org, then install the latest prerelease version:

bash
dotnet nuget add source https://f.feedz.io/sebastienros/fluid/nuget/index.json --name fluid-preview
dotnet add package Fluid.Core --prerelease

Keep NuGet.org enabled so dependencies can be restored. If your NuGet.config uses package source mapping, also map Fluid.* and MinimalApis.LiquidViews to the fluid-preview source.

Hello World

Source

csharp
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

Model security

Fluid templates can read public properties and fields from the model and from objects reachable through it. This is by design; a model passed to TemplateContext should be treated as the template's readable data boundary.

When rendering an untrusted template, pass a dedicated model that contains only the data the template is allowed to read. Do not pass domain entities, service objects, configuration objects, or other object graphs that may expose sensitive data through public members.

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:

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

Values registered in TemplateOptions.GlobalValues are shared by every context created from those options. Values set directly on a TemplateContext belong to that rendering. Custom tags that need temporary values should use a scope lease:

csharp
using var scope = context.EnterScope(ScopeBehavior.Local);
context.SetValue("temporary", value);

Local scopes inherit values and keep assignments local. WriteThrough scopes keep values assigned with LocalScope.SetOwnValue temporary while normal assignments update the caller, which matches include and loop behavior. Isolated scopes can only read the context's initial values and GlobalValues, which matches the render tag.


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.
  • Runtime MemberAccessStrategy.Register<T...> APIs are available for custom mappings.
  • No interceptor setup is required.

Recommended usage when targeting NativeAOT

  1. Reuse TemplateOptions instances (for example, at app startup).
  2. If you use runtime MemberAccessStrategy.Register<T...> calls, execute them during application startup before rendering templates.
  3. Pass a statically typed model and custom options to TemplateContext, or use [FluidRegister] for types that are not visible at a context construction site.
  4. Validate your app with AOT/trim publish settings:
bash
dotnet publish -c Release -r <RID> -p:PublishAot=true

Compatibility boundaries

NativeAOT compatibility and trimming compatibility are related but separate. Fluid's reflection fallback does not emit code and can run when dynamic code is unavailable. A trimmed application must also preserve every member that the fallback discovers at runtime.

Usage NativeAOT with trimming
new TemplateContext(concreteModel, customOptions) with the source generator enabled and matching compile-time and runtime model types Compatible. Eligible public fields and properties are accessed directly by generated code.
A model registered with [FluidRegister] Compatible. Use this for boxed models, nested model types, models created in another assembly, or types not visible at a TemplateContext construction site.
An explicit MemberAccessStrategy.Register<T...> mapping or custom MemberAccessor that accesses members directly Compatible. The application supplies the access logic instead of relying on member discovery.
The one-argument new TemplateContext(model) constructor or TemplateOptions.Default No accessor is inferred. Rendering uses an explicit registration if one exists; otherwise it falls back to reflection.
A model passed as object, an interface or base type whose runtime type differs, or an unregistered nested model No accessor is inferred for the runtime type. Use [FluidRegister] for the concrete runtime type or register an accessor explicitly.
Reflection fallback for an unregistered type NativeAOT-compatible only when the required public member metadata is preserved from trimming. Prefer source generation or explicit registration rather than relying on linker configuration.
A reflection-discovered Task<T> member Avoid in trimmed NativeAOT applications because the reflection fallback uses runtime dynamic binding to read the result. A generated or custom accessor handles Task<T> without dynamic binding.

Source generation covers public readable properties and public fields that can be referenced from generated code. Members that cannot be generated continue through the normal registration and reflection fallback paths. If any required member uses a fallback path, validate the published application rather than assuming that source generation preserved it.

Source generation (optional)

When the Fluid.SourceGenerator analyzer is enabled, Fluid can generate strongly-typed member accessors for model types discovered at compile time.

The model type is inferred automatically when its compile-time and runtime types match and it is passed with custom options:

csharp
var options = new TemplateOptions();
var context = new TemplateContext(person, options);

The generated accessor is activated on the DefaultMemberAccessStrategy of the options instance passed to that constructor. Explicit registrations on options.MemberAccessStrategy still take precedence. The one-argument TemplateContext(model) constructor does not infer or activate a model accessor because it uses the shared TemplateOptions.Default instance.

Use FluidRegisterAttribute when a model is passed as object, is created outside the compilation using the source generator, or when nested model types also need generated accessors. The recommended explicit pattern is to declare a custom TemplateOptions subclass and add one attribute per model type:

csharp
using Fluid;

[FluidRegister(typeof(Person))]
[FluidRegister(typeof(Address))]
public partial class PublicTemplateOptions : TemplateOptions
{
}

Use the generated options type like any other TemplateOptions instance:

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

csharp
options.MemberAccessStrategy.Register<Product, object>((product, name) => product.Name);

Custom member accessors

Custom accessors derive from MemberAccessor and return a ValueTask<FluidValue> directly.

csharp
private sealed class ProductDisplayNameAccessor : MemberAccessor
{
    public override ValueTask<FluidValue> GetAsync(
        object obj,
        string name,
        TemplateContext context)
    {
        return CreateValueTask(((Product)obj).Name, context);
    }
}

options.MemberAccessStrategy.Register<Product>(
    "display_name",
    new ProductDisplayNameAccessor());

T

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

C#dotnetliquidparsershopify

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言