Handlebars.Net & JSON templates

I ❤️ Handlebars! So I was very very very happy to see that Handlebars was ported to .NET! It is a mega flexible templating engine as it can easily be extended. I'm working on a projects where I need to parse objects via JSON templates to JSON strings. This blog will show how to instruct Handlebars to parse into JSON and add some nice error messages if your template fails.

Encoding JSON text

The trick is to add a special ITextEncoder to the IHandlebars instance. I've created a JsonTextEncoder that will do the proper escaping:

using HandlebarsDotNet;
using System.Text;

public static class JsonHandlebarsDotNet
{
    public class JsonTextEncoder : ITextEncoder
    {
        public void Encode(StringBuilder text, TextWriter target)
        {
            Encode(text.ToString(), target);
        }

        public void Encode(string text, TextWriter target)
        {
            if (text == null || text == "") return;
            text = System.Web.HttpUtility.JavaScriptStringEncode(text);
            target.Write(text);
        }

        public void Encode<T>(T text, TextWriter target) where T : IEnumerator<char>
        {
            var str = text?.ToString();
            if (str == null) return;
            Encode(str, target);
        }
    }

    public static IHandlebars Create()
    {
        var handlebars = Handlebars.Create();
        handlebars.Configuration.TextEncoder = new JsonTextEncoder();
        return handlebars;
    }

    public static HandlebarsTemplate<object, object> Compile(string template)
    {
        return Create().Compile(template);
    }
}

I've added some extra methods for convenience.

Example

You can use the static class to parse JSON templates and receive a JSON string back:

string source =
@"
{ 
  ""title"": ""{{title}}"",
  ""body"": ""{{body}}""
}";

var template = JsonHandlebarsDotNet.Compile(source);

var data = new
{
    title = "My new post",
    body = "This\nis\nmy \"first post\"!"
};

var result = template(data);

/* result is: 
{
  "title": "My new post",
  "body": "This\nis\nmy \"first post\"!"
}
*/

Improve JSON debugging

Debugging those JSON results are quite a challenge, as it is super easy to forget a comma. So we need something to help is detect validation errors quickly. Let's add a new method that also parses the result and wraps any exception:

public static string Parse(string template, object input)
{
    var t = Compile(template);
    var json = t(input);

    try
    {
        JsonConvert.DeserializeObject<dynamic>(json);
    }
    catch (Exception ex)
    {
        throw new InvalidJsonException(ex, json);
    }

    return json;
}

Let's implement the InvalidJsonException class that parsed the message of the exception that was thrown:

public class InvalidJsonException : Exception
{
    public string JsonText { get; }

    public InvalidJsonException(Exception ex, string jsonText) : base(ParseMessage(ex, jsonText), ex)
    {
        JsonText = jsonText;
    }

    public static string ParseMessage(Exception ex, string json, int linesAbove = 3, int linesUnder = 2)
    {
        var message = ex.Message;

        //Example: After parsing a value an unexpected character was encountered: ". Path 'title', line 4, position 4.
        var match = Regex.Match(message, @"line (?<line>\d+), position (?<position>\d+)");

        if (match.Success)
        {
            var line = int.Parse(match.Groups["line"].Value);
            var position = int.Parse(match.Groups["position"].Value);

            // add lines numbers
            var padding = (line + linesUnder).ToString().Length;
            var lines = json.Split("\n")
                .Select((str, index) => $"{(index+1).ToString().PadLeft(padding, '0') } | {str}")
                .ToList();

            // insert visual identifier
            lines.Insert(line, new string('-', position + padding + 3) + "^");

            // create final output
            var top = Math.Max(0, line - linesAbove);
            message += "\n\n" + String.Join("\n", lines.Take(line + linesUnder + 1).Skip(top));
        }


        return message;
    }
}

Now our errors contain a visual representation of where the error is, making debugging a lot easier. Hier we have an example of a template containing the error. We've omitted a , between the .NET and C# tags.

string source =
@"
{ 
  ""title"": ""{{title}}"",
  ""author"": ""Kees C. Bakker"",
  ""authorUrl"": ""https://keestalkstech.com/about-me/"",
  ""created"": ""2202-09-17 11:52"",
  ""categories"": [ ""programming"" ],
  ""tags"": [ "".NET"" ""C#"", ""HBS"" ],
  ""body"": ""{{body}}""
}";

var data = new
{
    title = "My new post",
    body = "First!"
};

try
{
    var json = JsonHandlebarsDotNet.Parse(source, data);
    Console.WriteLine(json);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

When we execute the code, we'll get the following error message:

After parsing a value an unexpected character was encountered: ". Path 'tags[0]', line 8, position 21.

06 |  "created": "2202-09-17 11:52",
07 |  "categories": [ "programming" ],
08 |  "tags": [ ".NET" "C#", "HBS" ],
-----------------------^
09 |  "body": "First!"
10 | }

One might consider pre-parsing the template for errors, but that won't help you, because the template might have perfectly valid handlebar tags, which are invalid in JSON.

Inject a JSON Template Generator

While a static class will work for many use cases, it might be better to use dependency injection. Let's create a IJsonTemplateGenerator:

using HandlebarsDotNet;

public interface IJsonTemplateGenerator
{
    IHandlebars Handlebars { get; }

    HandlebarsTemplate<object, object> Compile(string template);

    string Parse(string template, object input);

    dynamic? ParseToObject(string template, object input);
}

This object has everything to help us in parsing templates:

  • Handlebars to which the user could add custom helper extensions.
  • Compile to compile a string template and use it later on.
  • Parse to parse a template with data into a JSON string. This method should do the validation as well.
  • ParseToObject to parse a template with data and deserialize the result into a dynamic object.

Now, let's implement it:

using HandlebarsDotNet;
using Newtonsoft.Json;

public class JsonTemplateGenerator : IJsonTemplateGenerator
{
    public IHandlebars Handlebars { get; }

    public JsonTemplateGenerator()
    {
        Handlebars = HandlebarsDotNet.Handlebars.Create();
        Handlebars.Configuration.TextEncoder = new JsonTextEncoder();
    }

    public HandlebarsTemplate<object, object> Compile(string template) => Handlebars.Compile(template);

    public string Parse(string template, object input)
    {
        var t = Compile(template);
        var json = t(input);
        Deserialize(json);
        return json;
    }

    public dynamic? ParseToObject(string template, object input)
    {
        var t = Compile(template);
        var json = t(input);
        return Deserialize(json);
    }

    private static dynamic? Deserialize(string json)
    {
        try
        {
            // replace tabs with spaces, as tabs make the JSON in the
            // console unreadable:
            json = json.Replace("\t", "  ");

            return JsonConvert.DeserializeObject<dynamic>(json);
        }
        catch (Exception ex)
        {
            throw new InvalidJsonException(ex, json);
        }
    }
}

Manifest streams: templates as embedded resources

I would like to ship my templates as an embedded resource with the DLL. So let's create some extension methods to help us to

using System.Reflection;

namespace BotZero.Common.Templating;

public static class JsonTemplateGeneratorExtensions
{
    public static string ParseWithManifestResource(this IJsonTemplateGenerator generator, string name, object input)
    {
        return generator.ParseWithManifestResource(Assembly.GetCallingAssembly(), name, input);
    }

    public static dynamic? ParseWithManifestResourceToObject(this IJsonTemplateGenerator generator, string name, object input)
    {
        return generator.ParseWithManifestResourceToObject(Assembly.GetCallingAssembly(), name, input);
    }

    public static string ParseWithManifestResource(this IJsonTemplateGenerator generator, Assembly assembly, string name, object input)
    {
        var template = GetManifestTemplate(assembly, name, input);
        return generator.Parse(template, input);
    }

    public static dynamic? ParseWithManifestResourceToObject(this IJsonTemplateGenerator generator, Assembly assembly, string name, object input)
    {
        var template = GetManifestTemplate(assembly, name, input);
        return generator.ParseToObject(template, input);
    }

    private static string GetManifestTemplate(Assembly assembly, string name, object input)
    {
        if (assembly == null)
        {
            throw new ArgumentNullException(nameof(assembly));
        }

        using var stream = assembly.GetManifestResourceStream(name);
        if (stream == null) throw new Exception($"Manifest resouce with name '{name}' in assembly '{assembly.FullName}' not found.");

        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
}

We can now use these methods to read templates directly from our DLL:

var template = _generator.ParseWithManifestResourceToObject( 
    "BotZero.Handlers.Templates.Poker.json.hbs",
    vote);

Changelog

  • 2022-09-24: Added extension methods to parse manifest files.
  • 2022-09-24: Added the IJsonTemplateGenerator for dependency injection.
  • 2022-09-24: Added a ParseToObject to get a dynamic deserialized object.
  • 2022-09-18: Added line numbers to exception. Added a ValidateJson method to the final class.
  • 2022-09-17: Initial article.
expand_less