Monday, January 14, 2013

Using the Razor engine for rendering email templates

The problem. You want to have a flexible infrastructure to manage emails that are sent to users automatically.
The solution. First thing to deal with is making a text of an email body configurable. Hardcoding a template is not a good idea unless you want to recompile and redeploy your application each time you need to change the text. Templates can be stored in a database or in files, the exact solution depends on the situation. Second you need to find a way to insert a dynamic content to your emails. In this case you have to use placeholders in body text and replace them somewhere in the code.

Hello #FirstName# #LastName#!

string.Replace("#FirstName#", "Evgeny");
string.Replace("#LastName#", "Skurikhin");

This is an appropriate way only for very simple templates. But what if you need to render a table and you don't know the number of rows beforehand (only during runtime)? Well we still can use some kind of markers in email body. And what about conditional rendering? Hmm... another markers. But why reinvent the wheel? Let's use Razor.

After some research I found that the problem is quite common, there are multiple libraries that address the issue. RazorEngineRazorTemplates and RazorMachine are the most popular among developers. So I decided to try them all and choose the one that suits my needs.

Consider the following test class:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using RazorEngine;
using RazorTemplates.Core;
using Xipton.Razor;

namespace RazorEngineExample
{
    [TestClass]
    public class EngineTests
    {
        private const string CorrectResult =
            "<h1>FirstLevelField</h2><p>SecondLevelField</p><table><tr><td>ThirdLevelItem1</td></tr><tr><td>ThirdLevelItem2</td></tr><tr><td>ThirdLevelItem3</td></tr></table>";
        
        private readonly object fullyDynamicModel = new
        {
            FirstLevelValue = "FirstLevelField",
            NestedField = new
                {
                    SecondLevel = "SecondLevelField",
                    ThirdLevelList = new[]
                        {
                            new {Item = "ThirdLevelItem1"},
                            new {Item = "ThirdLevelItem2"},
                            new {Item = "ThirdLevelItem3"}
                        }
                }
        };

        private readonly object partiallyDynamicModel = new
        {
            FirstLevelValue = "FirstLevelField",
            NestedField = new
            {
                SecondLevel = "SecondLevelField",
                ThirdLevelList = new[]
                    {
                        new MyItem {Item = "ThirdLevelItem1"},
                        new MyItem {Item = "ThirdLevelItem2"},
                        new MyItem {Item = "ThirdLevelItem3"},
                    }
            }
        };

        private const string MyTemplate =
            "<h1>@Model.FirstLevelValue</h2><p>@Model.NestedField.SecondLevel</p><table>@foreach(var item in @Model.NestedField.ThirdLevelList){<tr><td>@item.Item</td></tr>}</table>";

        [TestMethod]
        public void RazorEngineFullyDynamicModelTest()
        {
            CheckResult(Razor.Parse(MyTemplate, fullyDynamicModel));
        }

        [TestMethod]
        public void RazorTemplatesFullyDynamicModelTest()
        {
            CheckResult(Template.Compile(MyTemplate).Render(fullyDynamicModel));
        }

        [TestMethod]
        public void RazorMachineFullyDynamicModelTest()
        {
            CheckResult(new RazorMachine().Execute(MyTemplate, fullyDynamicModel).Result);
        }

        [TestMethod]
        public void RazorEnginePartiallyDynamicModelTest()
        {
            CheckResult(Razor.Parse(MyTemplate, partiallyDynamicModel));
        }

        [TestMethod]
        public void RazorTemplatesPartiallyDynamicModelTest()
        {
            CheckResult(Template.Compile(MyTemplate).Render(partiallyDynamicModel));
        }

        [TestMethod]
        public void RazorMachinePartiallyDynamicModelTest()
        {
            CheckResult(new RazorMachine().Execute(MyTemplate, partiallyDynamicModel).Result);
        }

        private static void CheckResult(string actual)
        {
            Assert.AreEqual(CorrectResult, actual);
        }
    }

    public class MyItem
    {
        public string Item { get; set; }

    }
}

It contains two simple models. The first one is fully dynamic while the second one contains typed items in array. Each library is tested against both models using the same template.

All of them failed to render a fully dynamic model. RazorEngine and RazorMachine successfully rendered the template against the second model, while RazorTemplates didn't. In both cases RazorTemplates had problems with nested dynamic objects:


Test method RazorEngineExample.EngineTests.RazorTemplatesPartiallyDynamicModelTest threw exception:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'object' does not contain a definition for 'SecondLevel'.


Unfortunately when I added RazorEngine 3.0.8beta to our main project it crashed. The problem looked similar to this one. I wasn't able to make things work for partially dynamic models. It worked fine for fully typed models though.

RazorMachine 2.4.1 worked fine either in test or main solutions. So I decided to go with it.

2 comments: