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"