DEV Community

Ismail El Fekak
Ismail El Fekak

Posted on

Using DotLiquid to create a custom template in Asp.Net Core

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:

Project Creation Menu

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:

Project Structure

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>
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:
Run Screenshot

Now to use dynamic data, let's start by by adding a class named Employee to the Model directory, that should look like this:

Employee Model Structure

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;
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

After restarting the App we'll get something like this:

Run Preview

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Although the drop fields is case insensitive, know that the JSON fields are case sensitive.

And here's the preview:

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();
    }
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Then after restarting you'll get a preview like this:

Final preview

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)