allBlogsList

Using Razor Layouts With Insite Emails

Insite's EmailService allows a developer to easily send emails to a customer whenever they perform an action on the system. This EmailService utilizes the RazorEngine in order to parse HTML and generate rich content emails for delivery. On a recent project, I had many scenarios where an email was being sent to the customer. All of the emails contained a common look and feel, including a fully rendered header and footer. In this post, I will go over the process I followed in order to allow for the use of the standard Layout property in the Razor template for emails.

When the email templates were first created, I immediately tried to utilize the standard Layout property for a Razor view in order specify which content it should use.

Layout File:

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <style>
        .header, .footer, .body {
            border: solid thin black;
            height: 10%;
            width: 100%;
        }

        .body {
            margin: 10px;
        }
    </style>
</head>
<body>
    <header class="header">
        This is the common header for all of the emails sent from Insite
    </header>
    <div class="body">
        @RenderBody()
    </div>
    <footer class="footer">
        This is the common footer for all of the emails sent from Insite
    </footer>
</body>
</html>

Email Template

@{
    Layout = "~/Views/DefaultEmails/MainEmailLayout.cshtml";
}

<div class="email-wrapper">
    <div class="body-copy">
        <p>The password for your account on @Model.WebsiteUrl has been reset to:</p>
        <p><strong>@Model.NewPassword</strong></p>
        <p>Please visit <a href="@Model.WebsiteUrl">@Model.WebsiteUrl</a> to login and choose your own new password.</p>
    </div>
</div>

Unfortunately, this immediately started generating errors in the EmailService.

Error

Turns out, the relative path specified is a part of the Insite web application, and isn't resolvable from the context in which the EmailService executes. After looking through the OOTB EmailService code, I was able to determine that the content parser could be customized.

public class EmailServiceRio : EmailService
{
    public EmailServiceRio(IEmailTemplateUtilities emailTemplateUtilities, IContentManagerUtilities contentManagerUtilities, IEntityTranslationService entityTranslationService)
        : base(emailTemplateUtilities, contentManagerUtilities, entityTranslationService)
    {
    }

    // Insite apparently has issue with using Layout in the Razor.
    // We are overriding their parse template method to manually resolve and inject the Layout content.
    public override string ParseTemplate(string template, ExpandoObject model, string templateName)
    {
        #region Customize - Allow for Layouts in templates

        var noLineTemplate = template.Replace("\r", "").Replace("\n", "").Replace("\t", "");

        // @{    Layout = "~/Views/DefaultEmails/EmailMainLayout.cshtml"; }
        var layoutRegex = new Regex("@{\\s*?Layout\\s*?=\\s*?\"(?<filepath>.*?)\";\\s*?}");

        // If we are using a layout
        if (layoutRegex.IsMatch(noLineTemplate))
        {
            var filePath = layoutRegex.Match(noLineTemplate).Groups["filePath"].Value;

            noLineTemplate = layoutRegex.Replace(noLineTemplate, string.Empty);

            var layoutLines = string.Join(Environment.NewLine, File.ReadLines(HostingEnvironment.MapPath(filePath)).ToList());

            template = layoutLines.Replace("@RenderBody()", noLineTemplate);
        }

        #endregion

        var content = Razor.Parse(template, model, templateName);

        return content;
    }
}

The above code uses a Regular Expression and some basic string manipulation magic in order to detect and resolve a layout file being referenced in a email template. As mentioned above, the normal execution context of the EmailService means that it will not have access to a web based relative path to retrieve the content. In order to get the file, I need to get the fully qualified path for the layout file (e.g. C:\Projects\InsiteCommerce-4.2.0\InsiteCommerce.Web\Views\DefaultEmail\MainEmailLayout.cshtml). Once I have the fully qualified path for the layout, I can manually read the file off of the server. By simply replacing "@RenderBody()" from the layout file with the original template content, I now have a fully formed email ready to be parsed and delivered.

Email

There you have it. With the help of regular expressions and a some string manipulations, I now how a solution that allows me to utilize Razor Layouts in my email templates, saving me a lot of work when modifying the content of my emails.

The example used in this blog post has been implemented on Insite version 4.2.0.34172, a MVC application running on .NET Framework 4.5.2.