Fluid Liquid Template Engine for .NET

repository·main·Indexed 23 days ago

https://github.com/sebastienros/fluid

A high-performance Liquid template engine for .NET that allows safe execution of templates against .NET objects. It features fine-grained control over variable access, type conversion, and execution safety. Fluid supports ASP.NET Minimal APIs via MinimalApis.LiquidViews, custom value converters, strict mode for variables and filters, asynchronous template loading via ITemplateFileProvider, and Shopify-style money filters.

Tokens
8.4K
Snippets
26
Records
34
Agent score
82%

What's inside Fluid

  1. Analyze or alter templates using the Visitor pattern

    main

    Fluid provides a Visitor pattern to inspect or modify the Abstract Syntax Tree (AST) of a template.

    Visiting a template

    Use Fluid.Ast.AstVisitor to traverse the template. This is useful for security checks (e.g., verifying if a specific identifier is accessed) or auditing.

    Rewriting a template

    Use Fluid.Ast.AstRewriter to create a new version of the template with modified nodes. For example, you can replace one filter with another (e.g., replacing plus with minus).

    Automatic processing via TemplateParsed

    To apply visitors or rewriters to all templates (including includes and partials) automatically, use the TemplateParsed callback on TemplateOptions. This ensures the modified version is cached, improving performance.

    public class ReplacePlusFiltersVisitor : AstRewriter
    {
        protected override Expression VisitFilterExpression(FilterExpression filterExpression)
        {
            if (filterExpression.Name == "plus")
            {
                return new FilterExpression(filterExpression.Input, "minus", filterExpression.Parameters);
            }
            return filterExpression;
        }
    }
    
    // Usage
    var template = new FluidParser().Parse("{{ 1 | plus: 2 }}");
    var visitor = new ReplacePlusFiltersVisitor();
    var changed = visitor.VisitTemplate(template);
    var result = changed.Render(); // writes -1
  2. Define and use local functions with `macro`

    main

    The macro tag allows you to define reusable chunks of content that act as local functions within a template.

    Key Rules:

    • Macros must be defined before they are used in the template, as they are discovered during execution.
    • You can pass parameters to macros just like function arguments.

    Importing Macros: To use macros defined in an external template, you must explicitly import them using the from ... import syntax.

    {% macro field(name, value='', type='text') %}
    <div class="field">
      <input type="{{ type }}" name="{{ name }}"
             value="{{ value }}" />
    </div>
    {% endmacro %}
    
    {{ field('user') }}
    {{ field('pass', type='password') }}
    
    {% from 'forms' import field %}
    {{ field('user') }}
  3. Control whitespace in Liquid templates

    main

    By default, Liquid preserves all whitespace and newlines. You can control this using hyphens in tags or via global template options.

    Hyphen Syntax

    • {% tag -%}: Strips whitespace from the right side of the tag.
    • {%- tag %}: Strips whitespace from the left side of the tag.

    Global Options

    • TemplateOptions.Trimming: Set predefined preferences for automatic whitespace stripping.
    • TemplateOptions.Greedy: When false, only spaces before the first newline are stripped. When true (default), it follows standard Liquid behavior.
  4. How time zones work in Fluid (Parsing vs Rendering)

    main

    In Fluid, time zones behave differently depending on whether you are parsing or rendering:

    1. Parsing: The TimeZone property in TemplateContext and TemplateOptions is used to interpret date strings that lack explicit time zone information. If a string contains its own offset (e.g., +00:00), the context's TimeZone is ignored.
    2. Rendering: The TimeZone property does not affect rendering. When rendering a DateTime object, Fluid displays it in its own inherent time zone (usually UTC). To display a date in a different time zone during rendering, you must use the time_zone filter.

    Key takeaway: Setting TimeZone on the context helps Fluid understand input strings, but it won't automatically convert output dates.

  5. Organize Liquid views and partials

    main

    The view engine follows a specific file structure and naming convention:

    • Views Folder: The default location for all views and partials.
    • Partials: Can be placed in the Views folder or specifically in Views/Partials.
    • _layout.liquid: Acts as a template for multiple pages.
    • _viewstart.liquid: Executed for each view in the same folder (useful for setting a default layout).
    • Components: Files like component.liquid can be used as partial views.
  6. Use Layouts and Sections in Liquid

    main

    Fluid supports master templates (layouts) and content injection (sections).

    Layouts

    • Use {% layout 'path/to/layout.liquid' %} in a view to specify its master template.
    • Use {% renderbody %} in the layout to define where the view's content should appear.
    • Layouts can be defined globally using a _ViewStart.liquid file in the directory.

    Sections

    Sections allow a view to inject content into specific named areas of a layout (e.g., a menu or footer).

    • In the view: Use {% section name %} ... {% endsection %}.
    • In the layout: Use {% rendersection name %} to output the content.
    {# View #}
    {% layout '_layout.liquid' %}
    
    {% section menu %}
      <a href="#">Menu Link</a>
    {% endsection %}
    
    {# Layout #}
    <div class="menu">
      {% rendersection menu %}
    </div>
  7. Use Shopify-style money filters

    main

    Fluid implements Shopify money filters, but they must be explicitly registered in TemplateOptions.

    Registration:

    var options = new TemplateOptions();
    options.Filters.WithMoneyFilters();

    Available Filters:

    • money: Formats as currency (e.g., $1,134.65)
    • money_with_currency: Formats with currency code (e.g., $1,134.65 USD)
    • money_without_currency: Formats without currency symbol (e.g., 1,134.65)
    • money_without_trailing_zeros: Removes trailing zeros (e.g., $10)

    Configuration Options:

    • Cultures/Currencies: Formatting is derived from TemplateOptions.CultureInfo. You can set a default currency via options.MoneyOptions.Currency = "EUR"; or pass a currency to the filter: {{ 10 | money: 'GBP' }}.
    • Custom Currencies: Add new currencies to options.MoneyOptions.Currencies using MoneyCurrency.
    • Amounts in Cents: If your data uses cents (like Shopify), set options.MoneyOptions.AmountsInCents = true; to automatically divide inputs by 100.
    • Custom Formats: Use MoneyOptions.MoneyFormat and MoneyOptions.MoneyWithCurrencyFormat with Shopify-style placeholders like {{amount}}, {{currency}}, and {{currency_symbol}}.
    var options = new TemplateOptions();
    options.Filters.WithMoneyFilters();
    options.MoneyOptions.AmountsInCents = true;
    options.MoneyOptions.Currency = "EUR";
  8. Convert time zones during rendering with the time_zone filter

    main

    To display a DateTime in a specific time zone, use the time_zone filter.

    Important: You must combine the time_zone filter with the date filter. If you render a date without a format filter (e.g., {{ BirthDate }}), Fluid uses the ISO 8601 format which always displays in UTC, regardless of any timezone conversion applied.

    Usage Patterns

    Explicit Time Zone:

    {{ BirthDate | time_zone: 'America/New_York' | date: '%+' }}

    Using the 'local' keyword: Use 'local' to convert the value to the time zone configured in the current TemplateContext:

    {{ BirthDate | time_zone: 'local' | date: '%c' }}
  9. Optimize Fluid performance with caching and reuse

    main

    To achieve maximum performance with Fluid, follow these three best practices:

    1. Cache IFluidTemplate instances: Parsing is expensive. Instead of calling FluidParser.Parse() every time, cache the resulting IFluidTemplate instance. Use a unique key like the template name or content. It is recommended to use a singleton IMemoryCache with size limits and eviction policies to prevent unbounded memory growth. IFluidTemplate is thread-safe for read access.

    2. Reuse TemplateOptions: TemplateOptions contains shared state like property resolutions and lambdas. These are designed to be reused across multiple renderings. You can declare them as static or register them as a singleton in your DI container. TemplateOptions is thread-safe for read access.

    3. Reuse FluidParser: Instantiating a FluidParser is an expensive operation. Create one instance and reuse it throughout your application lifecycle (e.g., as a static instance or a singleton in DI).

    Summary of Thread Safety:

    • IFluidTemplate: Thread-safe for read access.
    • TemplateOptions: Thread-safe for read access.
    • FluidParser: Should be reused (singleton/static).
  10. Altering exposed .NET properties with ValueConverters

    main

    You can use TemplateOptions.ValueConverters to intercept .NET objects and return different values than those provided by the source model.

    • To return a custom value (like a formatted string) instead of the original type, return a FluidValue (e.g., StringValue).
    • To indicate that no conversion should be applied (allowing Fluid to use its default mapping), return null.

    You can also use this to wrap objects in a proxy class that inherits from ObjectValueBase to implement custom logic for specific property names.

    // Example: Converting DateTime to a custom string
    var options = new TemplateOptions();
    options.ValueConverters.Add(o => o is DateTime d ? new StringValue($"This is a date time: {d}") : null);
    
    // Example: Using a proxy to add virtual properties to an 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!");
            }
            return base.GetValueAsync(name, context);
        }
    }
    
    var options = new TemplateOptions();
    options.ValueConverters.Add(o => o is Person p ? new PersonValue(p) : null);
  11. Customize the MVC View Engine parser

    main

    To use custom tags within the ASP.NET MVC View Engine, create a class inheriting from FluidViewParser. Register your custom tags in its constructor, then assign this parser to the MvcViewOptions.

    public class CustomFluidViewParser : FluidViewParser
    {
        public CustomFluidViewParser()
        {
            RegisterEmptyTag("mytag", static async (s, w, e, c) =>
            {
                await w.WriteAsync("Hello from MyTag");
                return Completion.Normal;
            });
        }
    }
    
    // In Startup.cs
    services.Configure<MvcViewOptions>(options =>
    {
        options.Parser = new CustomFluidViewParser();
    });
  12. Enable parentheses for grouping expressions

    main

    By default, operators like and or or are evaluated from right to left, and filters are executed from left to right. You cannot use parentheses to change this order unless you explicitly enable the feature.

    To enable grouping with parentheses, set AllowParentheses to true in FluidParserOptions.

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

    Example enabled template:

    {{ 1 | plus : (2 | times: 3) }}