Walkthrough: Creating an HTML Email Template with Razor and Razor Class Libraries

Update 2020-06-20: Update for new Razor Class Library options for “support pages and views” option that is required for this to work.

Update 2020-04-18: As of .NET Core 3, ASP.NET Core targets .NET Core directly, instead of .NET Standard.  This means you cannot use a .NET Standard Class Library and reference ASP.NET Core.  Please see this repo for an example of getting Razor Emails working with .NET Core 3.1.  This post has been updated to just reference a .NET Core Library instead of .NET Standard Library.

tldr;

HtmlEmailExample

Full Code: https://github.com/scottsauber/RazorHtmlEmails

 

Usually I don’t blog walkthroughs and instead prefer to go a little deeper on a small topic, but I thought it would be useful to blog our approach on generating HTML emails using Razor for an ASP.NET Core insurance application at work.

 

If you’re looking to generate HTML Emails outside of ASP.NET Core (such as a Console app), I also recommend checking out Derek Comartin‘s post: Using Razor in a Console Application (outside of ASP.NET Core MVC).

 

Purpose

HTML emails are the absolute worst.  Inlined styles.  Nested tables.  Different apps and platforms render the markup differently.  It’s an absolute train wreck.  However, sending plain text emails isn’t a great way to impress your users and often makes emails look fake or like spam.  So fine, HTML emails.  Let’s do it.

 

What I would like to do is create an HTML Email Template library, and let developers focus on the content of their email, and not worry about the extra cruft that is HTML Emails.

 

Also, I want to be able to generate these HTML Emails from within a .NET Class Library, so that the sending of the emails happens right next to all my other business logic.

 

So at a high level, the requirements are:

  1. Create a base Email Layout to enforce a consistent layout (Header, Footer, base styles, etc.) and to hide the complexity of HTML Email Layouts.
  2. Create re-usable HTML Email components (such as buttons) to enforce consistent styling and hide the complexity of the implementation of the HTML Email components.
  3. Use Razor.
  4. Be able to call it from a .NET Core Class Library.

 

Razor checks the box for #1, because it already has the concept of a Layout view and a child view.  It also is a good fit for #2, because it lets you re-use UI components via Partials (among other methods).  In fact, you can actually achieve #1, #2, and #3 in regular ASP.NET 4.x fairly easily.  However, I haven’t ever been able to achieve #4 in regular ASP.NET or pre-2.1 ASP.NET Core.  That is, I want to use Razor in a non-ASP.NET/ASP.NET Core setting such as Class Libraries.

 

It’s super common for applications to put their business logic in a Class Library to remove any dependency on the UI project and to allow that logic to be re-used across other applications.  However, when I tried to send HTML Emails from a Class Library in ASP.NET 4.x and ASP.NET Core pre-2.1, I couldn’t figure out how to get the Class Library to find the Views I was creating and ultimately I gave up.

 

Enter Razor Class Libraries

I won’t go into much detail about Razor Class Libraries, when the documentation already does a fantastic job, but the basic idea behind them is that you can share UI between multiple ASP.NET Core applications.  This UI can be Views, Controllers, Razor Pages, etc.

 

The simplest way to think about Razor Class Libraries is if you add a View in your Razor Class Library, it essentially gets copied down into that same relative path into your main application.  So if you add an Index.cshtml file to the /Views/Home path of your Razor UI Class Library, then that file will be available at /Views/Home/Index.cshtml of your ASP.NET Core Application.  That’s pretty sweet and useful. But the question is would it find those files in a normal .NET Core Class Library?  The answer is – yes.

 

One potential gotcha: Make sure that your Views have unique names/paths.  If you make an HTML Email view that matches the path of an actual like MVC View or Razor Page, then they will conflict.  Therefore, I try to make my folder and/or view names clearly unique like “ConfirmAccountEmail.cshtml” which is unlikely to have a matching route on my ASP.NET Core application.

 

Alright then, let the walkthrough begin!

 

Create an ASP.NET Core Web Application

First – create an ASP.NET Core Web Application or you can use an existing ASP.NET Core Web App.  It doesn’t really matter what it is.

With Visual Studio:

  1. File
  2. New Project
  3. ASP.NET Core Web Application
    1. I just called it RazorHtmlEmails.AspNetCore
  4. Web Application
  5. OK

 

Or with the command line:

  1. dotnet new sln -n RazorHtmlEmails
  2. dotnet new razor -n RazorHtmlEmails.AspNetCore
  3. dotnet sln RazorHtmlEmails.sln add RazorHtmlEmails.AspNetCore

GitHub commit here

 

Create a Razor Class Library

Next – create an ASP.NET Core Razor Class Library.

With Visual Studio:

  1. Right-click the Solution
  2. Add
  3. New Project
  4. Razor Class Library
  5. Next
  6. Give it a name, I just called it RazorHtmlEmails.RazorClassLib
  7. Create
  8. Check the box for “Support pages and views” in the bottom right
    1. 2020-06-20_15-01-08
  9. Create

 

Or… with the command line (NOTE: the -s for “support pages and views”):

  1. dotnet new razorclasslib -n RazorHtmlEmails.RazorClassLib -s
  2. dotnet sln RazorHtmlEmails.sln add RazorHtmlEmails.RazorClassLib

 

After you’ve created your Razor Class Library, delete out the Areas folder, because we won’t need it.

GitHub commit here

 

Create the RazorViewToStringRenderer class

Tucked away in the aspnet/Entropy GitHub repo is the RazorViewToStringRenderer which shows you how to take a Razor View and render it to an HTML string.  This will be perfect for taking our Razor View, converting it to a string of HTML, and being able to stick the HTML into the body of an email.

 

Add this class to your Razor Class Library you just created.  I tucked mine under a folder called Services and then created an Interface for it called IRazorViewToStringRenderer:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace RazorHtmlEmails.RazorClassLib.Services
{
// Code from: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs
public class RazorViewToStringRenderer : IRazorViewToStringRenderer
{
private IRazorViewEngine _viewEngine;
private ITempDataProvider _tempDataProvider;
private IServiceProvider _serviceProvider;
public RazorViewToStringRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var view = FindView(actionContext, viewName);
using (var output = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return output.ToString();
}
}
private IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
{
return getViewResult.View;
}
var findViewResult = _viewEngine.FindView(actionContext, viewName, isMainPage: true);
if (findViewResult.Success)
{
return findViewResult.View;
}
var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join(
Environment.NewLine,
new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations)); ;
throw new InvalidOperationException(errorMessage);
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = _serviceProvider;
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
public interface IRazorViewToStringRenderer
{
Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model);
}
}

GitHub commit here

 

Create your base HTML Email Layout

The next step in the process is we are going to leverage Razor’s Layout system to create a base HTML Email Layout.  There are many HTML Email Layout templates out there so you don’t have to write one yourself (and trust me… if you’ve never seen HTML Email code before, you’re not going to want to write this yourself).

I’m picking this one from Litmus, which is the leading vendor  (as far as I know) for testing your HTML Emails on multiple different platforms, devices, and applications.  No they didn’t pay me to say that (although if someone from Litmus is reading this, it’d be cool if you did).

The layout looks like this:

HtmlEmailExample

 

However, all I really care about for the layout is everything outside of the white box for my layout.  Inside the white box will change based on whatever I’m sending (Email Registration Confirmations, Forgot Password requests, Password Updated Alerts, etc.).

 

HtmlEmailLayout

Steps:

  1. In your Razor UI Class Library, create a /Views/Shared folder
  2. Add an EmailLayout.cshtml to that folder
  3. Add the following code and try not to pass out

 

<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<style type="text/css">
/* FONTS */
@@media screen {
@@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
src: local('Lato Regular'), local('Lato-Regular'), url(https://fonts.gstatic.com/s/lato/v11/qIIYRU-oROkIk8vfvxw6QvesZW2xOQ-xsNqO47m55DA.woff) format('woff');
}
@@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
src: local('Lato Bold'), local('Lato-Bold'), url(https://fonts.gstatic.com/s/lato/v11/qdgUG4U09HnJwhYI-uK18wLUuEpTyoUstqEm5AMlJo4.woff) format('woff');
}
@@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 400;
src: local('Lato Italic'), local('Lato-Italic'), url(https://fonts.gstatic.com/s/lato/v11/RYyZNoeFgb0l7W3Vu1aSWOvvDin1pK8aKteLpeZ5c0A.woff) format('woff');
}
@@font-face {
font-family: 'Lato';
font-style: italic;
font-weight: 700;
src: local('Lato Bold Italic'), local('Lato-BoldItalic'), url(https://fonts.gstatic.com/s/lato/v11/HkF_qI1x_noxlxhrhMQYELO3LdcAZYWl9Si6vvxL-qU.woff) format('woff');
}
}
/* CLIENT-SPECIFIC STYLES */
body, table, td, a {
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table, td {
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
-ms-interpolation-mode: bicubic;
}
/* RESET STYLES */
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
}
table {
border-collapse: collapse !important;
}
body {
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
}
/* iOS BLUE LINKS */
a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
font-size: inherit !important;
font-family: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
}
/* MOBILE STYLES */
@@media screen and (max-width:600px) {
h1 {
font-size: 32px !important;
line-height: 32px !important;
}
}
/* ANDROID CENTER FIX */
div[style*="margin: 16px 0;"] {
margin: 0 !important;
}
</style>
</head>
<body style="background-color: #f4f4f4; margin: 0 !important; padding: 0 !important;">
<!– HIDDEN PREHEADER TEXT –>
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Lato', Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
We're thrilled to have you here! Get ready to dive into your new account.
</div>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<!– LOGO –>
<tr>
<td bgcolor="#539be2" align="center">
<!–[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]–>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td align="center" valign="top" style="padding: 40px 10px 40px 10px;">
<a href="http://litmus.com" target="_blank">
<img alt="Logo" src="http://litmuswww.s3.amazonaws.com/community/template-gallery/ceej/logo.png" width="40" height="40" style="display: block; width: 40px; max-width: 40px; min-width: 40px; font-family: 'Lato', Helvetica, Arial, sans-serif; color: #ffffff; font-size: 18px;" border="0">
</a>
</td>
</tr>
</table>
<!–[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]–>
</td>
</tr>
<!– HERO –>
<tr>
<td bgcolor="#539be2" align="center" style="padding: 0px 10px 0px 10px;">
<!–[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]–>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<tr>
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 40px 20px 20px 20px; border-radius: 4px 4px 0px 0px; color: #111111; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 48px; font-weight: 400; letter-spacing: 4px; line-height: 48px;">
<h1 style="font-size: 48px; font-weight: 400; margin: 0;">@ViewData["EmailTitle"]</h1>
</td>
</tr>
</table>
<!–[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]–>
</td>
</tr>
<!– COPY BLOCK –>
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!–[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]–>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<!– COPY –>
<tr>
<td bgcolor="#ffffff" align="left" style="padding: 20px 30px 40px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 18px; font-weight: 400; line-height: 25px;">
@RenderBody()
</td>
</tr>
</table>
<!–[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]–>
</td>
</tr>
<!– FOOTER –>
<tr>
<td bgcolor="#f4f4f4" align="center" style="padding: 0px 10px 0px 10px;">
<!–[if (gte mso 9)|(IE)]>
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
<tr>
<td align="center" valign="top" width="600">
<![endif]–>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
<!– NAVIGATION –>
<tr>
<td bgcolor="#f4f4f4" align="left" style="padding: 30px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<p style="margin: 0;">
<a href="http://litmus.com" target="_blank" style="color: #111111; font-weight: 700;">Dashboard</a> –
<a href="http://litmus.com" target="_blank" style="color: #111111; font-weight: 700;">Billing</a> –
<a href="http://litmus.com" target="_blank" style="color: #111111; font-weight: 700;">Help</a>
</p>
</td>
</tr>
<!– UNSUBSCRIBE –>
<tr>
<td bgcolor="#f4f4f4" align="left" style="padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<p style="margin: 0;">If these emails get annoying, please feel free to <a href="http://litmus.com" target="_blank" style="color: #111111; font-weight: 700;">unsubscribe</a>.</p>
</td>
</tr>
<!– ADDRESS –>
<tr>
<td bgcolor="#f4f4f4" align="left" style="padding: 0px 30px 30px 30px; color: #666666; font-family: 'Lato', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
<p style="margin: 0;">Contoso – 1234 Main Street – Anywhere, MA – 56789</p>
</td>
</tr>
</table>
<!–[if (gte mso 9)|(IE)]>
</td>
</tr>
</table>
<![endif]–>
</td>
</tr>
</table>
</body>
</html>

view raw
EmailLayout.cshtml
hosted with ❤ by GitHub

When you finish throwing up in your mouth, let’s look at a couple important bits. Line 165 has the RenderBody() call which is where the white box is and where our child view will be placed dynamically based on the email I’m sending.

 

Another interesting spot is line 142 where I’m dynamically pulling an EmailTitle property from ViewDataViewData allows us to pass messages from a child view up to the parent view.  In the email screenshot above, this is the “Welcome” hero text.

 

ViewData

 

In a real application, I would have pulled that Welcome text down to the child view as well, but I left that as a demonstration of the ability for the child view to dynamically change the parent EmailLayout.  Some more examples of what you could do with ViewData could be the child view wants to dictate which Logo to use or what color the background is, or literally anything you want.  This is simply just leveraging a feature that’s been part of MVC for a long time.

 

 

Now that we’ve finished the Email Layout, let’s look at adding a custom button component.

GitHub commit here

 

Create a HTML Email Button Partial for re-usability

The next thing I want to do is start to make reusable components via partial views in order to abstract away the complexity of certain HTML Email components, as well as always providing a consistent look to the end user.

 

A good example of this is the button:

EmailButton

 

All that really needs to be dynamic about this is the text and the link. That should be easy enough.

Steps:

  1. Under /Views/Shared add a EmailButtonViewModel.cs class
  2. Add the following code:
namespace RazorHtmlEmails.RazorClassLib.Views.Shared
{
public class EmailButtonViewModel
{
public EmailButtonViewModel(string text, string url)
{
Text = text;
Url = url;
}
public string Text { get; set; }
public string Url { get; set; }
}
}

 

namespace RazorHtmlEmails.RazorClassLib.Views.Shared
{
public class EmailButtonViewModel
{
public EmailButtonViewModel(string text, string url)
{
Text = text;
Url = url;
}
public string Text { get; set; }
public string Url { get; set; }
}
}

3. Add an EmailButton.cshtml file
4. Add the following code:

@using RazorHtmlEmails.RazorClassLib.Views.Shared
@model EmailButtonViewModel
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 20px 30px 60px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="#539be2">
<a href="@Model.Url" target="_blank" style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 15px 25px; border-radius: 2px; border: 1px solid #539be2; display: inline-block;">
@Model.Text
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>

view raw
EmailButton.cshtml
hosted with ❤ by GitHub

 

Now we can re-use buttons in our child views by simply doing this:

@await Html.PartialAsync("EmailButton", new EmailButtonViewModelModel("Confirm Account", "https://google.com&quot;))

view raw
ChildView.cshtml
hosted with ❤ by GitHub

 

That saves us from copying and pasting that crazy table code around every time we need a button for our Emails, which is useful.

GitHub commit here

Create your HTML Email Content

Now we’ve got all of our foundation pieces in place, let’s start building on top of them. The last thing we need to do in our Razor Class Library is add our custom email.

 

  1. Add a new folder under Views called Emails. This folder will house all of our custom HTML Emails.
  2. Add a _ViewStart.cshtml and add the following code so that every HTML Email has the EmailLayout.cshtml we created above as its parent layout
@{
Layout = "EmailLayout";
}

view raw
_ViewStart.cshtml
hosted with ❤ by GitHub

3. Add a ConfirmAccount folder under /Views/Emails.  This folder will house our ConfirmAccount email logic.

4. Add a ConfirmAccountEmailViewModel.cs file with the following code:

namespace RazorHtmlEmails.RazorClassLib.Views.Emails.ConfirmAccount
{
public class ConfirmAccountEmailViewModel
{
public ConfirmAccountEmailViewModel(string confirmEmailUrl)
{
ConfirmEmailUrl = confirmEmailUrl;
}
public string ConfirmEmailUrl { get; set; }
}
}

5. Add a ConfirmAccount.cshtml file with the following code:

@using RazorHtmlEmails.RazorClassLib.Views.Emails.ConfirmAccount
@using RazorHtmlEmails.RazorClassLib.Views.Shared
@model ConfirmAccountEmailViewModel
@{
ViewData["EmailTitle"] = "Welcome!";
}
<p>
We're excited to have you get started. First, you need to confirm your account. Just press the button below.
</p>
<br />
@await Html.PartialAsync("EmailButton", new EmailButtonViewModel("Confirm Account", Model.ConfirmEmailUrl))
<br />
<p>
If you have any questions, just reply to this email—we're always happy to help out.
</p>
<br />
<p>
The Contoso Team
</p>

 

Right now is where it all starts to come together, and you can see the power of being able to use Razor to build out our HTML emails.  On our day-to-day emails that we build out for the rest of our application, we no longer have to worry about gross table syntax or inline style craziness, we can just focus on the custom content that makes up that HTML Email.

GitHub commit here

 

Create a .NET Core Class Library

But one of the coolest things about this, is the fact that we can call this code from regular .NET Core) Class Libraries, and have it all “just work.”  This means we can share our email templates across multiple applications to provide the same look and feel across all of them, and have all that logic live right next to the rest of our business logic.

 

So let’s create a .NET Core Class Library.  If you want you can create a .NET Core Library and it’ll work as well.

With Visual Studio:

  1. Right-click the Solution
  2. Add
  3. New Project
  4. Class Library (.NET Core)
    1. I just called it RazorHtmlEmails.Common
  5. OK

 

Or with the command line:

  1. dotnet new classlib -n RazorHtmlEmails.Common -f netcoreapp3.1
  2. dotnet sln RazorHtmlEmails.sln add RazorHtmlEmails.Common

 

Then delete out Class1.cs

GitHub commit here

 

Call the RazorViewToStringRenderer in your business logic

Now it’s time to hook up the .NET Core Class Library to the Razor Class Library.

  1. In the .NET Core Class Library (.Common), add a reference to the Razor Class Library (.RazorEmailLib)
  2. Install MailKit, a fantastic cross-platform, .NET Core-friendly email library
  3. Create a class called RegisterAccountService that will house our business logic for creating the account and sending the email.
using MailKit.Net.Smtp;
using MimeKit;
using MimeKit.Text;
using RazorHtmlEmails.RazorClassLib.Services;
using RazorHtmlEmails.RazorClassLib.Views.Emails.ConfirmAccount;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace RazorHtmlEmails.Common
{
public class RegisterAccountService : IRegisterAccountService
{
private readonly IRazorViewToStringRenderer _razorViewToStringRenderer;
public RegisterAccountService(IRazorViewToStringRenderer razorViewToStringRenderer)
{
_razorViewToStringRenderer = razorViewToStringRenderer;
}
public async Task Register(string email, string baseUrl)
{
// TODO: Validation + actually add the User to a DB + whatever else
// TODO: Base URL off of ASP.NET Core Identity's logic or some other mechanism, rather than hardcoding to creating a random guid
var confirmAccountModel = new ConfirmAccountEmailViewModel($"{baseUrl}/{Guid.NewGuid()}");
string body = await _razorViewToStringRenderer.RenderViewToStringAsync("/Views/Emails/ConfirmAccount/ConfirmAccountEmail.cshtml", confirmAccountModel);
var toAddresses = new List<string> { email };
SendEmail(toAddresses, "donotreply@contoso.com", "Confirm your Account", body);
}
// TODO: In reality, you probably want to make an EmailService that houses this code, but #Demoware
private void SendEmail(List<string> toAddresses, string fromAddress, string subject, string body)
{
var message = new MimeMessage();
message.From.Add(new MailboxAddress(fromAddress));
foreach (var to in toAddresses)
{
message.To.Add(new MailboxAddress(to));
}
message.Subject = subject;
message.Body = new TextPart(TextFormat.Html)
{
Text = body
};
using (var client = new SmtpClient())
{
// For demo-purposes, accept all SSL certificates
client.ServerCertificateValidationCallback = (s, c, h, e) => true;
client.Connect("127.0.0.1", 25, false);
client.Send(message);
client.Disconnect(true);
}
}
}
public interface IRegisterAccountService
{
Task Register(string email, string baseUrl);
}
}

The interesting bits are lines 25, where we create our ConfirmAccountEmailViewModel with the link we want to use and line 27 where we pass that model into our RazorViewToStringRenderer, and get back our Email’s HTML body.

 

As an aside, for testing local emails without an email server, I love using Papercut.  It’s a simple install and then you’re up and going with an email server that runs locally and also provides an email client.  No Papercut didn’t pay me to say that either.  It’s free.  If some paid service that does the same thing as Papercut does and wants to sponsor this blog, feel free to reach out to me and get rejected because you will never get me to give up Papercut.

GitHub commit here

 

Hook it up to the UI

The last step we have to do is to hook this up to the UI.  I’m just going to hook this up on a GET request of the /Index page just for demo purposes.  In reality, you’d have an input form that takes registration and have this happen on the POST.

 

  1. In your .AspNetCore project, add a project reference to .Common
  2. In Startup.cs under ConfigureServices wire up the IRegisterAccountService and the IRazorViewToStringRendererto their implementations.
// In Startup.cs in the ConfigureServices method
services.AddScoped<IRegisterAccountService, RegisterAccountService>();
services.AddScoped<IRazorViewToStringRenderer, RazorViewToStringRenderer>();

view raw
Startup.cs
hosted with ❤ by GitHub

3. In /Pages/Index.cshtml.cs, call the IRegisterAccountService in the OnGetAsync method

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorHtmlEmails.Common;
namespace RazorHtmlEmails.AspNetCore.Pages
{
public class IndexModel : PageModel
{
private readonly IRegisterAccountService _registerAccountService;
public IndexModel(IRegisterAccountService registerAccountService)
{
_registerAccountService = registerAccountService;
}
public async Task<IActionResult> OnGetAsync()
{
// In reality, you would have this on a POST and pass along user input and not just have the Confirm Account link be the Index page… but #Demoware
await _registerAccountService.Register("testmctestyface@contoso.com", Url.Page("./Index"));
return Page();
}
}
}

view raw
Index.cshtml.cs
hosted with ❤ by GitHub

GitHub commit here

 

And you’re done!  When I run the app, and open up Papercut, I get my email in all its glory.

RazorEmail

 

Going forward, anytime I need an email, I can simply leverage the EmailLayout and EmailButton infrastructure I’ve created to make creating HTML emails incredibly easy and less table-y which is a huge win.

 

Just a reminder if somehow you’ve made it this far, all the code is out here on GitHub.

 

Hope this helps!

88 thoughts on “Walkthrough: Creating an HTML Email Template with Razor and Razor Class Libraries

  1. it doesnt work when i am trying to call it from Web API. the problem cannot find view in GetView or FindView method always back with null

    Like

  2. Hi Scott,
    Great solution, however I hit one issue,
    If I call _razorViewToStringRenderer.RenderViewToStringAsync a few times in a row will be fine.
    But if I call
    _razorViewToStringRenderer.RenderViewToStringAsync and in the middle I call others services, when I call
    _razorViewToStringRenderer.RenderViewToStringAsync again the httpContext.RequestServices always zero, exception caught is the serviceprovider is disposed.
    Have you found similar issues to this?

    Many thanks
    Jim

    private ActionContext GetActionContext()
    {
    var httpContext = new DefaultHttpContext();
    httpContext.RequestServices = _serviceProvider;
    return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
    }

    Like

    • Hey Jim,

      I have not hit this issue. What I would look for is to see if one of your services is using the IServiceProvider directly and manipulating it in some way. It being disposed is a little odd. Any chance you could share code of what’s going on so I could take a deeper look?

      Like

      • Hi Scott, many thanks for your quick reply, no the other service does not use IServiceProvider directly. And one thing if the service use UserManager and the the ServiceProvider injected into the RazorViewToStringRendered constructor will be null. I will get the source code soon to share. Thanks again.

        Liked by 1 person

      • This will work:
        var infoEmail = _configuration.GetSection(“Email:InfoEmail”).Value;
        var body = await _emailTemplateService.RenderViewToStringAsync(“/Views/Template/ConfirmAccountEmail.cshtml”, model);
        var body2 = await _emailTemplateService.RenderViewToStringAsync(“/Views/Template/ConfirmAccountEmail.cshtml”, model);

        await _postmarkEmailService.SendAsync(notification.Email, “Confirm your registration”, body);
        await _postmarkEmailService.SendAsync(infoEmail, “New member registration”, body2);

        This will NOT work:
        var infoEmail = _configuration.GetSection(“Email:InfoEmail”).Value;
        var body = await _emailTemplateService.RenderViewToStringAsync(“/Views/Template/ConfirmAccountEmail.cshtml”, model);
        await _postmarkEmailService.SendAsync(notification.Email, “Confirm your registration”, body);

        var body2 = await _emailTemplateService.RenderViewToStringAsync(“/Views/Template/ConfirmAccountEmail.cshtml”, model);
        await _postmarkEmailService.SendAsync(infoEmail, “New member registration”, body2);

        this is the postmarkService only use Iconfiguration, and not only postmarkService, most of the services, the problem is with hpptcontext.RequestServices which assigned by serviceprovider.

        public class PostmarkEmailService : IPostmarkEmailService
        {
        protected readonly IConfiguration _config;
        private readonly PostmarkClient _client;
        private readonly ILogger _logger;
        private readonly bool _emailEnabled;
        private readonly string _from;
        private readonly string _bcc;
        private readonly string _emailOverride;

        public PostmarkEmailService(IConfiguration config
        //ILoggerFactory loggerFactory
        )
        {
        //_logger = loggerFactory.CreateLogger();
        _config = config;
        _client = new PostmarkClient(_config.GetSection(“Email:PostmarkApiKey”).Value);
        _emailEnabled = _config.GetSection(“Email:Enabled”).Value.ToLower() == “true”;
        _from = _config.GetSection(“Email:From”).Value;
        _bcc = null == _config.GetSection(“Email:Bcc”) ? string.Empty : _config.GetSection(“Email:Bcc”).Value;
        _emailOverride = null == _config.GetSection(“Email:Override”) ? string.Empty : _config.GetSection(“Email:Override”).Value;

        }

        Many thanks
        Jimy

        Like

  3. So this essentially still requires an ASP project? How do you test it? How do you get the dependencies it needs in a test project?

    Like

  4. Scott, you rock. I just implemented this in an Asp.Net Core Web API that sends email notifications, and it’s so much better than what I was doing before.

    Liked by 1 person

  5. Just want to say this walk-through was extremely helpful in implementing email sending in my app!

    One tip I would like to pass on: Do your styles properly with styles defined in the header like it was a standard web-page – then run it through an inliner such as PreMailer (free – on Nuget) and it will magically move all those styles in line.

    I updated RazorViewToStringRenderer to include such a call and it works great!

    Like

  6. This is absolutely awesome, I got this up and running in no time. I included PreMailer.Net to inline my css styles in my RazorViewToStringRenderer so I can design my templates using classes, it worked very well for me!

    Liked by 1 person

  7. Hi great write up and works striaght out of the box as it is.
    However I’ve been trying to implement this into a WebAPI application. Application structure is MVC talking to a WebAPI backend. However when I call the _viewEngine.GetView this never finds my view? any help would be great

    Like

      • Hey Matheus, do you happen to have a repro of the problem that I could take a look? Is it because in ConfigureServices there’s only `services.AddControllers` and not `services.AddControllersWithViews` ?

        Like

      • The problem in my case, was the project type. It won’t work if it’s not a Razor Class Library, despite having all the correct imports… I tryed adding this behavior to my current project, which uses Class Library (.NET Core), and that’s why GetView and FindView didn’t work.

        Like

  8. Hi! Thank you so much for this.
    I have an old Asp.Net MVC application which I want to add beautiful email templates too but since your walk through is for .net core is there a way for me to adapt it to my project.
    Thanks in anticipation.

    Like

  9. Hi Scott!
    Great post!
    The only thing I want to ask, are there any possible performance issues, having 500+ of email?
    Should I think on replacing services.AddScoped to singleton, or caching the template somehow?
    What will be the most expensive operation in your solution and how to speed it up?
    Thanks a lot!

    Like

    • Hey Danyl – you should be able to make it a Singleton in theory, but I haven’t tried it. As far as performance, I would assume making it a Singleton is pretty negligible, to construct this since there’s not much going on, but I would benchmark it if that matters to your scenario.

      I would assume the slowest part will be generating the HTML from the Razor template, but not much you can really do there since that logic happens inside the RazorViewEngine from ASP.NET Core. This is definitely one of those things where you are trading off less performance for improved developer productivity and long-term maintenance. So if your situation requires performance, then you may have to re-evaluate that trade off and just deal with housing the HTML logic in strings or something rather in Razor.

      I would just try the 500 emails at once though and set a baseline and see if that baseline is good enough for your scenario.

      Like

  10. I’m a newbie trying to follow your steps and it seems that some of your steps are missing, like maybe a screenshot is not rendering. You say:

    Add the following code:

    3. Add an EmailButton.cshtml file
    4. Add the following code:

    Now we can re-use buttons in our child views by simply doing this:

    Like

      • Randy – no problem! Was just circling back to this and glad to see all is well. Curious if there’s anything I can do to make them not show up as false positives for ads… if you have any ideas let me know! Thanks.

        Like

  11. I’m trying to render a razor view from a aspnetcore 2.2 BackgroundService. Essentially I have a service sleeping in the background that wakes up and sends out emails of various types: Appointment Reminders, Reports to stake holders etc, etc.

    Your code works great until I try to render a view that has TagHelpers in it. When the engine tries to resolve the tag helpers it throws an ArgumentOutOfRange exception.

    I think the problem is that the HttpContext that is created in your code has no routing information in it, and the TagHelper requires that routing info to translate ‘asp-controller’ and ‘asp-action’ into html.

    I have seen other people use IHttpContextAccessor to get a legit context, but naturally the returned context is always null in the context of a Background Service (Makes sense to me, unless I’m missing something!?).

    Any thoughts on how to solve this? I hate to hardcode links in every view that I could potentially want to render to a string.

    Here is the stacktrace:
    at System.ThrowHelper.ThrowArgumentOutOfRange_IndexException()
    at System.Collections.Generic.List`1.get_Item(Int32 index)
    at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.get_Router()
    at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.GetVirtualPathData(String routeName, RouteValueDictionary values)
    at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.Action(UrlActionContext actionContext)
    at Microsoft.AspNetCore.Mvc.UrlHelperExtensions.Action(IUrlHelper helper, String action, String controller, Object values, String protocol, String host, String fragment)
    at Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.GenerateActionLink(ViewContext viewContext, String linkText, String actionName, String controllerName, String protocol, String hostname, String fragment, Object routeValues, Object htmlAttributes)
    at Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper.Process(TagHelperContext context, TagHelperOutput output)
    at Microsoft.AspNetCore.Razor.TagHelpers.TagHelper.ProcessAsync(TagHelperContext context, TagHelperOutput output)
    at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.d__0.MoveNext()
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
    at AspNetCore.Views_Shared__email_Layout2.<b__35_1>d.MoveNext() in C:\Workspace\GroomShop\GroomShop.Web\Views\Shared\_email_Layout2.cshtml:line 144

    Like

  12. To avoid the exception being generated in the above message, I have modified GetActionContext() to have a Router in its collection as seen below. This solves the exception problem but unfortunately now the tag-helpers incorrectly resolve to href=””. Hopefully someone has seen this before and has a recommendation / solution?

    private ActionContext GetActionContext()
    {
    var httpContext = new DefaultHttpContext
    {
    RequestServices = this._serviceProvider,
    };

    var attrRouter = this._serviceProvider.GetRequiredService();

    var rd = new RouteData();
    //rd.Values.Add(“controller”, “Email”);
    //rd.Values.Add(“action”, “AppointmentReminder”);
    rd.Routers.Add(attrRouter);

    var result = new ActionContext(httpContext, rd, new ActionDescriptor());

    return result;
    }

    Like

  13. i am getting an invalid operation exception : InvalidOperationException: The model item passed into the ViewDataDictionary is of type ‘HTMLEmails.Views.Emails.ConfirmAccount.ConfirmAccountEmailViewModel’, but this ViewDataDictionary instance requires a model item of type ‘NetCoreApp.Models.LoginViewModel’. although i did every thing exactly the same.
    using asp.net core 2.2

    Like

    • Problem solved!
      there was a conflict between the view start of the original project and the view start of the HTML emails project, moving the latter view start to the emails folder solved the problem.

      Thanks for the great post!

      Like

  14. THANK YOU!!!
    I’ve always thought that it had to be “easy” to get Razor views to work with server side rendering to a string, but I have had one heck of a time finding a way to do this. In my opinion this needs to be written up on MSDN as the “official” way to do templating in .NET Core. I’m not using this for emails but for HTML report to PDF generation. It’s rather easy to take a HTML document and go to PDF but the missing link I had was being able to make the HTML on the server side and handing it off to the PDF renderer. This solves that problem in such an elegant way!

    Like

    • Hey Mike – I really appreciate the comment. That’s a great point about HTML => PDF and is a great use case for this type fo thing as well. Thanks again for taking the time to leave the comment, I really appreciate it.

      Like

  15. This is very helpful and works great for sending HTML emails.

    I’m trying a similar approach for plain text emails (actually multi-part HTML/plain text) by setting up a second RazorViewEngine for the text version that automatically looks for .text.cshtml versions of the views for the plain-text versions.

    Everything is working well except there seems to be no way to create a RazorViewEngine that isn’t geared towards HTML as an HtmlEncoder is required in its constructor. As a result, everything in my plain-text views must be wrapped with Html.Raw() or new HtmlString() to avoid being HTML-encoded, otherwise characters like + in model values end up escaped as HTML entities in the plain text part of the email. Raw/new HtmlString works, but it really clutters up the plain-text Razor views and it seems kinda redundant to encode only to then unencode the model values. Implementing a custom HtmlEncoder seems challenging and likely to be error-prone as well because a lot of the encoding helper classes that the default HtmlEncoder uses are internal.

    I would be interested to hear if anyone has any other approaches for using Razor with plain-text templates.

    Liked by 1 person

  16. Hey Scott thank you for this tutorial its the best solution i found so far and it works like a charm the only issue is that i cant seem to find a way to use localization (po or resx) for the RazorHtmlEmail.RazorClassLib. Im not a .net pro but ive been searching for hours, a link to a tutorial could be useful. Stay awesome Scott!

    Like

  17. Hi Scott. Thanks so much for this blog, it solves a multitude of problems in an elegant way. However, I’m struggling to get a way to embed links to graphics inside the emails. The IUrlHelper seems to be generated from the ActionContext and this is devoid of any route data. I’ve been struggling to find a way to upgrade this method to include a valid router but have not yet found a way. Do you have a solution to this?

    Like

  18. Great tutorial! Is it possible to get the rendered markup out without it being escaped? The output for tags like looks like this <h1>.

    Like

  19. Hi Scott,
      I am trying to implement the email service with templates in Razor with ModelView, however when trying to load the Model by its method, I get the error: “Index was out of range. Must be non-negative and less than the size of the collection . \ r \ nParameter name: index “.
      Can you help me understand what this error could be, as it seems to me that he is not able to load the model in the view.

    var viewContext = new ViewContext (
                            actionContext,
                            view,
                            new ViewDataDictionary (
                                metadataProvider: new EmptyModelMetadataProvider (),
                                modelState: new ModelStateDictionary ())
                            {
                                Model = model
                            },
                            new TempDataDictionary (
                                actionContext.HttpContext,
                                _tempDataProvider),
                            output,
                            new HtmlHelperOptions ());

      The exception is giving this line marked with -> await view.RenderAsync (viewContext);

    Like

  20. I have seen that others also have said that had i similar problem, with the render not finding the view.
    But i did not found a single solution to this problem.

    You said that it was possible to use a asp.net core 3 library as a .RazorClassLib, but does not seem to work for me.

    I have downloaded your project and looked at the diffrence between your project and mine.

    I could find two ‘major’ differences:
    This first one is that your are still using MailKit(2.4.1) instead of NETCORE.MailKit(2.0.2) in your github repo.

    And the second one is that you have an additional framework in your RazorCallsLib; you have the Microsoft.AspNetCore.App. (I only have the Microsoft.NETCore.App)

    Like

    • For some odd reason i can not edit or remove my comment, here is my edit:

      I have seen that others also have said that they had a similar problem, with the render not finding the view.
      But i did not found a single solution to this problem.

      You said that it was possible to use a asp.net core 3 library as a .RazorClassLib, but does not seem to work for me.

      I have downloaded your project and looked at the diffrence between your project and mine.

      I could only find two ‘major’ differences:
      This first one is that your are still using MailKit(2.4.1) instead of NETCORE.MailKit(2.0.2) in your github repo.

      And the second one is that you have an additional framework in your RazorCallsLib; your project has the Microsoft.AspNetCore.App. (I only have the Microsoft.NETCore.App)

      Like

  21. Hello Scott,
    Thank you for a good solution to render emails! Unfortunately I don’t quite get it to work. I downloaded your two libraries (RazorHtmlEmail.Common and RazorHtmlEmails.RazorClassLib) and added them to my project so the code should be correct. But when I try to reference it in my original project it does not recognize the names (CS0246: The type or namespace name ‘RazorHtmlEmails’ could not be found (are you missing a using directive or an assembly reference?)

    Here is a small snippet of my code where the error occurs: https://freeimage.host/i/error.J0CegS

    I noticed that your placeholder project for using the libraries has the same name (RazorHtmlEmails), could this be why my project does not find them?

    My project uses .NET Core 3.0

    Like

    • Oscar, the latest examples show .NET Core 3.1. I would suspect that could be your problem if you have project references to the other two projects. .NET Core 3.0 dropped out of support in March so I would recommend upgrading to 3.1 anyways, and it’s pretty painless. Let me know if that helps!

      Like

  22. This worked like a charm once I created my Razor Class Library correctly, thank you!

    A note for anyone creating a Razor Class Library with Visual Studio 2019. On the last screen before it is created you have to make sure you select the “Support pages and views” checkbox for this to work. It isn’t checked by default and the proper dependencies for the RazorViewToStringRenderer to work correctly will not be included if it isn’t checked.

    Liked by 1 person

    • Thanks for letting me know Keith! I have updated the tutorial to reflect this. I think this may have been a recent addition with the Blazor WASM release, because I haven’t seen this in 3.1 until now.

      Like

  23. I want to make it work for dynamic model since I wont be able to create separate ViewModels for all emails. When I changed TModel to dynamic, this line
    view.RenderAsync(viewContext) threw
    An exception of type Microsoft.CSharp.RuntimeBinder.RuntimeBinderException occurred in System.Private.CoreLib.dll but was not handled in user code
    object’ does not contain a definition for Name

    Like

  24. Hi, great walk-thru. My projects are Core 2.1. Is there anything different I need to do to get it to work in Core 2.1. Currently it cannot find my Views. When I created the RCL project it doesn’t have the option for “support pages and views” . How can I set this in 2.1 so that it has the correct references needed for the render ?

    Like

    • Hi Margarita,

      I upgraded this walk through to 3.1 a few months ago. The GitHub repo still has the history though for when I made this walkthrough for 2.1 – https://github.com/scottsauber/RazorHtmlEmails/commits/master.

      You can either checkout to a few commits back or look through the last few commits and workout what needs undone. There isn’t a ton to change to be honest. I would suspect it has to do with your csproj.

      If you have a GitHub repo that reproduces the problem, I’d be happy to help.

      Like

Leave a Reply to tarunupadhyaya Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s