Render Emails Using Razor Templating

June 28, 2022
Written by
Dave Glick
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

In the previous post, you went inside the Razor engine to learn how it works and understand what goes on when you turn a Razor file into HTML. You examined the four phases of parsing, code generation, compilation, and execution and how your C# code ends up being mixed together with your raw markup and output content. In this post, you’ll take things a step further and put that knowledge to use by comparing some open-source implementations of the Razor engine. You'll also learn how to use  these Razor engines in your applications and render email templates to send emails with a service like SendGrid.

The Different Kinds Of Razor Implementations

Recall from the previous post that when you talk about Razor as a templating language, and even as an engine, you’re really talking about two different things: the underlying low-level Razor engine that’s responsible for the four execution phases, and a higher-level implementation of that engine that adds conventions like partials, sections, tag helpers, etc. This distinction is important because it allows third-party Razor libraries to implement the language their way, with their own advantages and drawbacks.

Most open-source Razor libraries either implement their engine on top of the more general-purpose engine and attempt to replicate or clone conventional Razor features like layouts and partials to varying degrees of completeness and success. Some implementations choose to wrap one of the existing out-of-the-box implementations in a way that makes it usable from outside the intended ASP.NET Core environment. Depending on the approach taken, the end result might not be very different and similar features may be supported, but it’s important to understand how each library works because it will influence what you can accomplish with that library.

Open-Source Razor Libraries

Given that you now know how Razor works, and how there are lower-level vs. higher-level abstractions, you can examine some open-source libraries and how they make using Razor from your own code easier for various use cases. Each of these libraries has their own strengths and weaknesses and the “best” one to use depends on the Razor features that you need, and your particular use case.

For the purposes of comparison, you’ll be rendering the same simple Razor template in all the discussed libraries:

<div>Hi @Model.Name, the date and time is @System.DateTime.Now</div>

This template lets us look at how to set up a model with the template, as well as how to mix C# code into the content by rendering the current date and time.

RazorEngineCore

One of the earliest open-source libraries to expose Razor outside the Microsoft web frameworks was RazorEngine. It hasn’t been updated in four years, and a port of that library to .NET Core called RazorEngine.NetCore has also stalled out without commits in about two years. That said, the spirit of the original RazorEngine lives on in an active project called RazorEngineCore which, rather than try to port and adapt the old RazorEngine code, has been built from the ground-up with .NET Core support.

RazorEngineCore makes use of the Razor compiler, but does not leverage any higher-level functionality present in the ASP.NET Core Razor implementation. This means that while it’s extremely simple (which may be exactly what you need), you won't be able to leverage some conventional Razor features you might be used to like sections, partials, and layouts. That’s because while RazorEngineCore uses the Razor parser and compiler to combine your raw output with C# code in your template and generate an executable assembly, it doesn’t layer on or attempt to replicate any of these higher-level features. On the other hand, using RazorEngineCore is extremely straightforward. Here’s an example:

using RazorEngineCore;

string template = "<div>Hi @Model.Name, the date and time is @System.DateTime.Now</div>";

IRazorEngine razorEngine = new RazorEngine();
IRazorEngineCompiledTemplate compiledTemplate = razorEngine.Compile(template);
string result = compiledTemplate.Run(new
{
    Name = "Dave"
});

Support for strongly-typed models is also provided, though it requires a slightly more verbose syntax since the model type needs to be provided to the call to IRazorEngine.Compile() as a generic type parameter. There are more examples available in the repository, including how to save and load compiled templates, as well as how to cache them.

RazorLight

RazorLight is similar to RazorEngineCore in that it uses the low-level Razor engine and implements higher-level functionality directly. Unlike RazorEngineCore, RazorLight was developed from scratch for .NET Standard and .NET Core.

RazorLight has equally simple syntax to RazorEngineCore and the code you prefer probably comes down to personal preference:

using RazorLight;

string template = "<div>Hi @Model.Name, the date and time is @System.DateTime.Now</div>";

RazorLightEngine engine = new RazorLightEngineBuilder()
    .UseEmbeddedResourcesProject(Assembly.GetEntryAssembly())
    .Build();
string result = await engine.CompileRenderStringAsync(
    "cacheKey",
    template,
    new
    {
        Name = "Dave"
    });

If you look closely, you’ll notice a few key differences in that code. The first is that we need to initialize a “project system”. This is how RazorLight finds other files your templates might reference like layouts or partials (more on these below). In this case, an embedded resource project system was configured which tells RazorLight to look for external templates as embedded resources of the specified assembly. Another difference is that RazorLight includes a built-in caching engine and a unique cache key is required when rendering a template. If the same cache key is used in a future call to the compilation and rendering methods, the same cached compilation can be used.

I mentioned above that RazorLight supports layouts and partials too. However, because it does not leverage the higher-level Razor functionality provided by ASP.NET Core.  These features are all implemented internally with varying degrees of compatibility. The use of layouts closely mirrors the syntax in ASP.NET Core flavors of Razor, but partials are implemented differently using something called “includes”.

Comparing the particular features provided in ASP.NET Core vs. RazorEngineCore vs. RazorLight is out of scope for this post. The important thing is to understand that even though each of these Razor implementations leverage the same underlying Razor engine, parser, and code generator, the way that they use it and the additional features they layer on top can be quite different. This provides both a bit of confusion but also an opportunity to customize and tailor a particular flavor of Razor to specific use cases, as each of these libraries has done.

Statiq

The previous libraries you’ve looked at all attempt to provide a thin API surface on top of the low-level Razor engine. While not trivial, the Razor engine is written to support this kind of usage and these libraries all become even more “flavors” of Razor. However, another possible approach is to wrap the entire ASP.NET Core Razor implementation and provide that to callers in an easy-to-use way. Unfortunately, that’s easier said than done since unlike the underlying Razor engine, the ASP.NET Core implementation of Razor isn’t really intended to be extended. Using it directly requires some reverse engineering, reflection, and other trickery.

Statiq is a .NET static site generator that attempts to achieve feature parity with Razor features from ASP.NET Core that .NET developers are familiar with. In order to achieve this, it does wrap the entire ASP.NET Core Razor implementation and calls it to render Razor templates. While the Statiq Razor code isn’t currently designed to be completely isolated from Statiq itself, creating a bare-bones Statiq engine and then leveraging it to render a Razor template is fairly straightforward:

using Statiq.Common;
using Statiq.Core;
using Statiq.Razor;

string template = "<div>Hi @Model.Name, the date and time is @System.DateTime.Now</div>";

ServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddRazor();
Engine engine = new Engine(serviceCollection);
engine.Pipelines.Add("Razor").ProcessModules.AddRange(
    new CreateDocuments(template),
    new SetMetadata("Name", "Dave Glick"),
    new RenderRazor());
await engine.ExecuteAsync();
string result = await engine.Outputs.FromPipeline("Razor")[0].GetContentStringAsync();

Because of the extra overhead of creating a Statiq engine and working with Razor inside the conventions of Statiq (notice that a model isn’t used here and “metadata” is used instead), this code seems quite different from the previous code. Because of this, it might not be appropriate for most simple use cases, particularly when concerned mostly with rendering simple email templates. However, this does show a relatively terse way of getting access to the full ASP.NET Core Razor engine for runtime template rendering when and if you should need it.

In-Process ASP.NET Core

So far you’ve looked at libraries that either use or wrap the Razor engine, but there’s another approach that’s worth considering if you really need support that’s as close to the default ASP.NET Core implementation as possible. It’s possible to host an entire ASP.NET Core web application in-process and issue requests to it at runtime, all from within your application. In this model, you can create your Razor templates as if they were normal ASP.NET Core pages (which they are) and then use an HttpClient to call your in-process ASP.NET Core web server to retrieve the rendered content. The code for this is an exercise for the reader, but keep in mind it’s an option should you absolutely need ASP.NET Core Razor rendering and no libraries have your required feature set.

How to Render a Razor Template to send Emails

Now you’ve seen three different libraries with varying compatibility with Razor features from ASP.NET Core, and you’ve read code for each that shows how to render a Razor template into a string. In this final section, you’ll combine that knowledge with the SendGrid API to render a simple email template and send the output as an email.

Before you go any further though, it’s important to make sure that your SendGrid account is configured to send emails. Follow the instructions in the “Configure your SendGrid account to send emails” section of this blog post. Once your account is properly configured, come back here to continue learning how to leverage Razor for your email templates.

There are a few ways to interact with SendGrid including using the SendGrid API, but the easiest way to use SendGrid in a C# application is to use the SendGrid .NET library. Note also that while this example uses SendGrid, the same concepts could be applied to any email service.

First, create a new console application:

dotnet new console -n SendGridRazor

This command creates a new console application called SendGridRazor and places it in a folder with the same name. Navigate to the new folder and add the SendGrid NuGet package:

cd SendGridRazor
dotnet add package SendGrid

Now that you have a console application with the SendGrid library installed, you’ll need to store your SendGrid API key in an environment variable called SENDGRID_API_KEY so your application can read it without storing it directly in code.

If you're using bash or a similar shell, run this command:

export SENDGRID_API_KEY={your key}

If you're using PowerShell on Windows or another OS, use this command:

$Env:SENDGRID_API_KEY = "{your key}"

If you're using CMD on Windows, use this command:

set "SENDGRID_API_KEY={your key}"

Once your project is set up and your API key has been stored as an environment variable, open up the Program.cs in the SendGridRazor folder and replace its content with the following:

using SendGrid;
using SendGrid.Helpers.Mail;

var apiKey = Environment.GetEnvironmentVariable("SENDGRID_API_KEY");
var client = new SendGridClient(apiKey);

The code above gets your API key from the environment variable you created earlier and then creates a SendGrid client instance.

This code uses the new top-level statements feature in C# that doesn’t require a Main method and Program class. If you’re using an older version of .NET, this code should go inside your Program.Main method.

Once you have a client, you can define the sender, recipient, and subject:

var from = new EmailAddress("[SENDER_ADDRESS]", "[SENDER_NAME]");
var to = new EmailAddress("[RECIPIENT_ADDRESS]", "[RECIPIENT_NAME]");
var subject = "This Message Is From My Razor Template";

Replace the placeholders [SENDER_ADDRESS] and [SENDER_NAME] in the code above with the email address and name of your SendGrid Sender and do the same for [RECIPIENT_ADDRESS] and [RECIPIENT_NAME] for the recipient of your email.

With those out of the way, it’s time to consider how to render your template. You can use a similar template to the one used above for comparing Razor libraries and pass in the recipient name from the same EmailAddress object you created for the SendGrid client. You could use any of the previous libraries we examined, but this example will use RazorLight.

Add the RazorLight NuGet package to your project by typing this in the command line:

dotnet add package RazorLight

This command will install RazorLight along with a number of dependencies. You’ll also need to add a few more required namespaces to the top of your Program.cs file once the package is installed:

using System.Reflection;
using RazorLight;

You also need to add the PreserveCompilationContext property to your project file for RazorLight to work properly when using recent .NET SDKs. Open the SendGridRazor.csproj file and add the following in between the <PropertyGroup> and </PropertyGroup> elements near the top of the file:

<PreserveCompilationContext>true</PreserveCompilationContext>

And finally, add the following code to your Program.cs file underneath where you defined the sender and recipient of your email to use RazorLight to render your email template:

string template =
    "<div>Hi @Model.Name, the date and time this email was sent is @System.DateTime.Now</div>";

RazorLightEngine engine = new RazorLightEngineBuilder()
    .UseEmbeddedResourcesProject(Assembly.GetEntryAssembly())
    .Build();
string htmlContent = await engine.CompileRenderStringAsync(
    "MyEmailTemplate1",  // cache key
    template,
    new
    {
        Name = to.Name
    });

That provides some HTML content as rendered by Razor, but there’s one final step. Most emails also include a plain-text version just in case the recipient can’t format HTML emails. You should create an alternate plain-text template. This can be constructed any way you want, using Razor without any HTML elements or perhaps just using string interpolation. Here’s an example using a string to produce some similar text content as the Razor code:

string textContent = $"Hi {to.Name}, the date and time this email was sent is {DateTime.Now}";

Lastly, add the following lines to send the email using SendGrid:

var msg = MailHelper.CreateSingleEmail(from, to, subject, textContent, htmlContent);
var response = await client.SendEmailAsync(msg).ConfigureAwait(false);

Bringing this all together looks like this:

using System.Reflection;
using RazorLight;
using SendGrid;
using SendGrid.Helpers.Mail;

string template =
    "<div>Hi @Model.Name, the date and time this email was sent is @System.DateTime.Now</div>";

// Create the SendGrid client
var apiKey = Environment.GetEnvironmentVariable("SENDGRID_API_KEY");
var client = new SendGridClient(apiKey);

// Create the from/to/subject strings
var from = new EmailAddress("[SENDER_ADDRESS]", "[SENDER_NAME]");
var to = new EmailAddress("[RECIPIENT_ADDRESS]", "[RECIPIENT_NAME]");
var subject = "This Message Is From My Razor Template";

// Render the Razor content template to HTML
RazorLightEngine engine = new RazorLightEngineBuilder()
    .UseEmbeddedResourcesProject(Assembly.GetEntryAssembly())
    .Build();
string htmlContent = await engine.CompileRenderStringAsync(
    "MyEmailTemplate1",  // cache key
    template,
    new
    {
        Name = to.Name
    });

// Create some similar plain-text content
string textContent = $"Hi {to.Name}, the date and time this email was sent is {DateTime.Now}";

// Send the email
var msg = MailHelper.CreateSingleEmail(from, to, subject, textContent, htmlContent);
var response = await client.SendEmailAsync(msg).ConfigureAwait(false);

To run your application, type the following in the command line:

dotnet run

This should run the application and send the email through SendGrid with your rendered Razor template as the body. If you set yourself as a recipient, you should receive an email that looks like this:

Email with subject "This Message Is From My Razor Template" and body "Hi Example Recipient, the date and time this email was sent is 6/23/2022 9:20:37 AM"

Next Steps

Now you know how to use a Razor template to render and send an email message at runtime in under 20 lines of code. Some improvements might be storing the template in a file or embedded resource, caching the template compilation, or using a layout to add your logo or other email header. All of these are left as exercises for the reader, but be aware the various Razor libraries might have built-in support for some of these features too. Also be aware that HTML emails can only use a subset of HTML, so it’s important to only use supported elements in your Razor templates if you’re using them to render emails.

As mentioned at the very beginning of this series, Razor isn’t the only templating language and may not even be a good fit depending on your particular use case. If the idea of using a templating language seems interesting but you’d like to explore some other options, try reading Send Emails with C#, Handlebars templating, and Dynamic Email Templates. Likewise, the approach described here isn’t the only way to send emails using Razor either. If you’d like to explore another library that leverages Razor for sending email, read How to send Emails in C# .NET with FluentEmail, Razor, and SendGrid.

Happy sending!

Dave Glick has been professionally developing software for nearly two decades, most recently on the .NET platform. He is a Microsoft MVP in the Visual Studio and Development Technologies category and is currently the Principal Software Engineer for a small non-profit where he architects and leads the development of line of business applications, data forensics tools, and DevOps infrastructure. He is passionate about open source and its community, publishes several projects of his own, and has contributed to many others.