Skip to content

Liquid.NET for Developers

Mike Bridge edited this page Nov 16, 2015 · 37 revisions

Although the Liquid.NET templating language is very close to Shopify Liquid, the Liquid.NET C# code is very different from the Shopify Ruby implementation.

The Quick Start Guide is a good place to start to see what Liquid.NET C# code looks like.

Basic Template Parsing & Rendering

The three basic steps to generate a liquid template are:

  1. Create a LiquidTemplate from a string using LiquidTemplate.Create()
  2. Create a context that contains the template's state, including your data
  3. Merge your data from the context into the LiquidTemplate using LiquidTemplate.Render()
// 1) parse the template and check for errors
var parsingResult = LiquidTemplate.Create("<div>{{myvariable}}</div>");

if (parsingResult.HasParsingErrors)
{
    MyErrorHandler(parsingResult.ParsingErrors);
    return;
}

// 2) create a template context that knows about the standard filters,
// and define a string variable "myvariable"
var ctx = new TemplateContext()
    .WithAllFilters()
    .DefineLocalVariable("myvariable", LiquidString.Create("Hello World"));

// 3) merge the variables from the context into the template and check for errors
var renderingResult = parsingResult.LiquidTemplate.Render(ctx);
if (renderingResult.HasParsingErrors)
{
    HandleErrors(renderingResult.ParsingErrors);
    return;
}
if (renderingResult.HasRenderingErrors)
{
    HandleErrors(renderingResult.RenderingErrors);
    return;
}

Console.WriteLine(renderingResult.Result);

===> "<div>Hello World</div>"

Syntactic Sugar for Error Handling

That first example has a lot of ceremony to handle errors. Here is a more concise way of expressing the same thing (in 0.9.7):

// create a place to accumulate parsing and rendering errors.
var errors = new List<LiquidError>();

// Note that you will still get a best-guess LiquidTemplate, even if you encounter errors.
var liquidTemplate = LiquidTemplate.Create("<div>{{myvariable}}</div>")
    .OnParsingError(errors.Add)
    .LiquidTemplate;

// [add code here to handle the parsing errors, return]

var ctx = new TemplateContext()
    .WithAllFilters()
    .DefineLocalVariable("myvariable", "Hello World".ToLiquid());


// The final String output will still be available in .Result, 
// even when parsing or rendering errors are encountered.
var result = liquidTemplate.Render(ctx)
    .OnAnyError(errors.Add) // also available: .OnParsingError, .OnRenderingError
    .Result;

// [add code here to handle the parsing and rendering errors]

Console.WriteLine(renderingResult);

===> "<div>Hello World</div>"

Types

Types in liquid are numeric, boolean, string, collection, hash, date, and the special type range. These are represented with C# classes that implement ILiquidValue. There is also the value nil, which has no liquid type.

liquid C#
boolean LiquidBoolean
numeric LiquidNumeric
date LiquidDate
string LiquidString
collection LiquidCollection
hash LiquidHash
range* LiquidRange

Ranges are currently only available in a few places---most of the rest of this discussion doesn't apply to them. More on this later.

LiquidHashes and LiquidCollections are heterogeneous, meaning that a collection can hold a mix of types, and although the keys of a hash are always strings, a value in a hash can also be of any type.

A numeric has an underlying C# type, which is int, long, decimal or BigInteger. You can create a LiquidNumeric with the static constructor LiquidNumeric.Create(val). This means that if you create a numeric value with the decimal "3.00", it will render as "3.00". If you do math with two numeric values, it will attempt to use the precision of the most precise value, so an "int" LiquidNumeric times a "decimal" LiquidNumeric will yield a decimal LiquidNumeric. If you try to use non-supported numeric types like unsigned integers, floats or doubles, they will be coerced into one of the four supported types.

Add Values to the TemplateContext

There are a few different ways to add your LiquidValue to the TemplateContext. You can create a value and store it using DefineLocalVariable(String key, ILiquidValue val)

new TemplateContext()
    .DefineLocalVariable("myvar1", LiquidString.Create("Hello"))
    .DefineLocalVariable("myvar2", LiquidString.Create("World"));

Note that the TemplateContext is where all the state is kept when you render a LiquidTemplate. You can re-render the same LiquidTemplate instance with different TemplateContext once it's been created, but you should always discard a TemplateContext and create a new one each time you render a LiquidTemplate.

If you're wondering why the word Local appears in DefineLocalVariable, it's because it's defining it locally in a symbol table on a stack so that it appears in the right scope. When we define a variable from here, it's in the global scope.

You can also define several variables simultaneously with one data structure by passing in an IDictionary:

var templateContext = new TemplateContext().WithLocalVariables(
    new Dictionary<String,Option<ILiquidValue>> {
        { "test", LiquidString.Create("TEST")}
    });

You can also create values with the object extension method ToLiquid(). ToLiquid() will attempt to transform an object into the correct Liquid type, e.g.:

// create a LiquidString
ITemplateContext ctx = new TemplateContext()
   .DefineLocalVariable("mystring", "Hello World".ToLiquid());

// create a LiquidNumeric
ITemplateContext ctx = new TemplateContext()
   .DefineLocalVariable("poco", 33.ToLiquid());

// create a LiquidHash with a LiquidString field.
var myPoco = new MyPocoObject{ MyString = "Test String" }
ITemplateContext ctx = new TemplateContext()
   .DefineLocalVariable("poco", myPoco.ToLiquid());

Filters

See How to Write a Filter for more details.

Tags

See How to Write a Tag or Block for more details.