Introduction
Liquid is an open-source template language created by Shopify and written in Ruby. It can be used to add dynamic content to pages, and to create a wide variety of custom templates. While DotLiquid is a templating system ported to the .NET framework from Ruby’s Liquid Markup.
Even though the fact that Liquid is vastly used, the documentation for DotLiquid isn't that exhaustive and finding guides for specific use cases might not bear any fruits.
I myself struggled a bit while trying to use it, especially with complex JSON objects but at the end I've finally got some success.
We'll move directly to how to use DotLiquid to create a template in Asp.Net Core (.Net 6).
Setup
First we'll need a working .net project, using your favorite IDE create a new Asp.Net Core Web App using the MVC template:
If you prefer using CLI type in this command in the directory where you want to add your project:
dotnet new mvc -au None
Then using either Nuget Package Manager or the CLI, we'll have to add the DotLiquid package
dotnet add package DotLiquid
and the
NewtonSoft.Json package to the project
dotnet add package Newtonsoft.Json
Demo
On creation our project would look like this:
First in our wwwroot
(or webroot) folder let's create a template directory and add a template file to it, which are files that end with the .liquid
extension and use a combination of objects, tags, and filters to make the content dynamic.
For short it's a normal HTML
syntax + some extra syntax for the logic.
Let's name our file example.liquid
, and for the moment we'll put a single div
in it, to test that the template rendering works.
<div>
<h3>The template content</h3>
</div>
Then in the HomeController
let’s add the logic to render a template to its Index
Action Method:
public IActionResult Index()
{
// we need to read the contents of the template file
string liquidTemplateContent = System.IO.File.ReadAllText("wwwroot/template/example.liquid");
// then we parse the contents of the template file into a liquid template
Template template = Template.Parse(liquidTemplateContent);
// then we render the template
string result = template.Render();
// and then we return the result to the view in a view bag object
ViewBag.template = result;
return View();
}
Then all we need to do is put our template in our view file, overwrite the content of Views/Index.cshtml
with the following:
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
@Html.Raw(ViewBag.template)
</div>
Here we used the @Html.Raw
helper tag to render the html inside the template
Finally run your project to see the result, that should look like this:
Now to use dynamic data, let's start by by adding a class named Employee to the Model directory, that should look like this:
Assuming we want to render employees data in our template, to enforce security DotLiquid doesn't let you access a Models attribute directly, as explained in their docs:
DotLiquid is focused on making your templates safe. It does this by making sure templates can only access properties and methods that have been specifically enabled.
This means that you can't just pass a model instance to the template and access the properties directly.
So to enable the properties we'll have to create a Drop
object which uses an opt-in approach to expose data.
A Drop
class is a class that inherits from the DotLiquid Drop
class and exposes the data we want to pass to our template.
Let's add the following to our Employee
class:
public class EmployeeDrop : Drop
{
private readonly Employee _employee;
public EmployeeDrop(Employee employee)
{
_employee = employee;
}
public string Name => _employee.Name;
public string Email => _employee.Email;
public string Phone => _employee.Phone;
}
I omitted adding the address
for the moment to show that if we don't add an attribute to the drop the template wouldn't be able to output it and instead would show a blank string.
Then in the HomeController
we need to make the following change to the Index
action method:
public IActionResult Index()
{
// we need to read the contents of the template file
string liquidTemplateContent = System.IO.File.ReadAllText("wwwroot/template/example.liquid");
// then we parse the contents of the template file into a liquid template
Template template = Template.Parse(liquidTemplateContent);
// then we create a new instance of the model class
Employee employee = new Employee
{
Name = "John Doe",
Email = "john.doe@example.com",
Phone = "555-555-5555",
Address = "123 Main St."
};
Hash hash = Hash.FromAnonymousObject(new
{
employee = new EmployeeDrop(employee)
});
// then we render the template
string result = template.Render(hash);
// and then we return the result to the view in a view bag object
ViewBag.template = result;
return View();
}
Let's not forget adding the data to our example.liquid
template:
<div>
<h3>The template content</h3>
<p>
Name: {{ employee.name }}
</p>
<p>
Email: {{ employee.email }}
</p>
<p>
Phone: {{ employee.phone }}
</p>
<p>
Address: {{ employee.address }}
</p>
</div>
After restarting the App we'll get something like this:
Notice that we got an empty address as we didn't expose it in the drop.
Next let's do just that, but instead of a normal string
let's say we have a JSON object so we'd cover one of the most recurring problems nowadays.
In the EmployeeDrop
let's add the Address
attribute like so, we'll use NewtonSoft.Json
to deserialize the JSON.
public class EmployeeDrop : Drop
{
// ... old code
public IDictionary<string, object>? Address =>
JsonConvert.DeserializeObject<IDictionary<string, object>>(_employee.Address);
}
Then let's change the address format to a valid JSON in the Index
action method:
public IActionResult Index()
{
// ... old code
var address = @"
{
""Street"" : ""123 Main St"",
""City"" : ""Anytown"",
""State"" : ""WA"",
""Zip"" : ""12345""
}
";
Employee employee = new Employee
{
Name = "John Doe",
Email = "john.doe@example.com",
Phone = "555-555-5555",
Address = address
};
// ... old code
return View();
}
The last step is to use the new attribute inside our template:
<div>
<h3>The template content</h3>
<p>
Name: {{ employee.name }}
</p>
<p>
Email: {{ employee.email }}
</p>
<p>
Phone: {{ employee.phone }}
</p>
<p>Address:</p>
<p>Street: {{ employee.address.Street }}</p>
<p>City: {{ employee.address.City }}</p>
<p>State: {{ employee.address.State }}</p>
<p>Zip: {{ employee.address.Zip }}</p>
</div>
Although the drop fields is case insensitive, know that the JSON fields are case sensitive.
And here's the preview:
It might seem like you can render any JSON like this but if we tried deserializing a complex object and using it in our template all we'd get for the nested fields are blank strings.
Let's say an Employee
can have multiple addresses and we have a JSON object containing an array of addresses, let's make the following changes to the Index
action method:
public IActionResult Index()
{
// old code
var addresses = @"
{
""Addresses"": [{
""Address"": ""123 Main Street"",
""City"": ""Montreal"",
""State"": ""QC"",
""Zip"": ""H1S1M5"",
""Country"": ""Canada""
},
{
""Address"": ""456 Main Street"",
""City"": ""Montreal"",
""State"": ""QC"",
""Zip"": ""H1S1M5"",
""Country"": ""Canada""
}
]
}";
Employee employee = new Employee
{
Name = "John Doe",
Email = "john.doe@example.com",
Phone = "555-555-5555",
Address = addresses
};
// ... old code
return View();
}
To make our template recognize the Nested JSON we need to use a custom Dictionary Converter with our deserializer, here you'll find one that you can use, you can add it as is to the Employee
class and then modify the Address
attribute in EmployeeDrop
to use it like so:
public class EmployeeDrop : Drop
{
// ... old code
public IDictionary<string, object>? Address =>
JsonConvert.DeserializeObject<IDictionary<string, object>>(_employee.Address, new DictionaryConverter());
}
The final step is to change the template:
<div>
<h3>The template content</h3>
<p>
Name: {{ employee.name }}
</p>
<p>
Email: {{ employee.email }}
</p>
<p>
Phone: {{ employee.phone }}
</p>
<p>Addresses:</p>
<div>
{% for addr in employee.address.Addresses -%}
<div>
<p>Street: {{ addr.Street }}</p>
<p>City: {{ addr.City }}</p>
<p>State: {{ addr.State }}</p>
<p>Zip: {{ addr.Zip }}</p>
<p>Country: {{ addr.Country }}</p>
</div>
{% endfor %}
</div>
</div>
Then after restarting you'll get a preview like this:
Conclusion
I hope you'll find some answers on how to create a DotLiquid template in Asp.Net Core in this article. I didn't cover the usual control flow or iteration tags as the documentation of the Liquid templates already has all the basics and what's nice in DotLiquid is that you can use almost anything from the liquid template language as is, you can check their docs here.
You'll find the final code on this Github repository.
Top comments (0)