Back to Main Site

Contents

Filter Reference

Using Fluid as a Data Scripting Language

At version 2.17, Fluid gained a property called Assigned on TemplateContext. This is a delegate that executes every time the assign command is used inside the template.

It is possible to use this to allow for lightweight “data-centric” scripting with Fluid. Scripts could be written and execute inside your C# application to provide the ability for your users to “program” with it.

To use the Assigned delegate:

context.Assigned = (identifier, value, context) =>
{
  // Do something amazing…
};

Any time a variable is explicit assigned inside an executing template using the assign keyword, the supplied delegate will execute.

Using this, we can store assigned values “outside” of the template, in a simple dictionary. After your template has executed, the dictionary will contain all the of values assigned.

Combining this with the liquid tag will allow users to write scripts which can extend C# code. These scripts can be executed and the C# code can act on the resulting values.

// "script" is a set of Fluid commands
// "data" is a string/object dictionary with a set of existing objects

// Embed the script in some "liquid" tags to make it parseable
var source = @$"
{{% liquid
{script}
%}}";
_parser.TryParse(source, out var template, out var error);

// Assign the incoming data to variables
var context = new TemplateContext();
foreach (var item in data)
{
  context.SetValue(item.Key, item.Value);
}

// Set the delegate to replace the dictionary variables
// if anything is assigned
context.Assigned = (identifier, value, context) =>
{
  data[identifier] = value.ToObjectValue();
  return ValueTask.FromResult(value);
};

// We don't capture the output, because we don't care about it…
await template.RenderAsync(context);

// "data" is now populated with (potentially) script-modified values

Using this code, a Dictionary<string,object> can be loaded with data. Then a set of Fluid statements can be executed (with no brackets; we’re not generating output remember), and may or may not alter the data inside the dictionary. After the script has executed, the dictionary values can be read out and acted on.

Here’s a quick use case –

Assume we have a content management system (CMS). Whenever content is saved, our CMS can execute user-written scripts to act on the content before it is written back to the repository.

For example, imagine our administrators want to make sure that no one adds exclamation points to the end of the title of an article (Mary tends to be so dramatic…)

This script would prevent that:

if title endswith "!"
  assign title = title | replace:"!",""
endif

Using a custom filter, we could simplify it:

assign title = title  | trimend:"!"

Or we could cancel the operation

if title endswith "!"
  assign cancel = true
  assign message = "Title cannot be so dramatic…"

  if username == "mary"
    assign message = message | append:" Dammit Mary, we've talked about this…"
  endif
endif

Whenever content was saved, we could assign the title and the body of an article to the data dictionary, execute this “script” via the code above, then read out the title and body from the dictionary. They might have been changed by the script, or they might not (this is no different from using EventArgs in an event handler).

I have abstracted this into a class: LiquidScriptData. It’s here

It works like this:

var data = new LiquidScriptData()
{
    ["first_name"] = "Annie",
    ["last_name"] = "Barker"
};

// Understand that this script wouldn't be hard-coded in C#
// (I mean, what would be the point? Just do it in C# then…)
// This is something that would be stored in some repository
// as a user-managed string.
var keysThatChanged = data.Evaluate(@"
    if first_name == 'Annie'
        assign first_name = 'Deane'
    endif
");

// data["first_name"] now equals "Deane"
// keysThatChanged contains one value: "first_name"