Send HTML email using Razor templates

Recently I had to come up with a solution for email generation within a system, the main requirement being that we needed a template system to manage and populate the resulting email content. It occurred to me that Razor templates already do what I need so I started investigating using strongly types partial views as a way to generate HTML for email.

The first hurdle is that the MVC pipeline and template engines all rely on a controller context, which is fine if you you are sending email from a controller action. But most busy sites will need to queue or automatically generate emails on a scheduled basis.

I stumbled across an old blog post which described how to achieve the first part of what I was looking for, and allow me to get the rendered HTML output from a partial view and model.

The trick is to create a dummy controller context so the view engine can do its stuff and render the view as normal.

public static T CreateController<T>(RouteData routeData = null) where T : Controller, new()
{
    // create a disconnected controller instance
    T controller = new T();

    HttpContextBase wrapper = null;
    if (HttpContext.Current != null)
    {
         wrapper = new HttpContextWrapper(System.Web.HttpContext.Current);
    }
    else
    {
        throw new InvalidOperationException("Can't create Controller Context if no active HttpContext instance is available.");
    }

    if (routeData == null)
        routeData = new RouteData();

    // add the controller routing if not existing
    if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller"))
    {
        routeData.Values.Add("controller", controller.GetType().Name.ToLower().Replace("controller", ""));
    }

    controller.ControllerContext = new ControllerContext(wrapper, routeData, controller);

    return controller;
}

The resulting HTML can be returned using this method from a given view path and model.

public string RenderViewToString(string viewPath, object model) 
{
ControllerContext context = CreateController<GenericController>().ControllerContext;
// first find the ViewEngine for this view, note assumes only partials ViewEngineResult viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null); // get the view and attach the model to view data var view = viewEngineResult.View; context.Controller.ViewData.Model = model; string result = null; using (var sw = new StringWriter()) { var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw); view.Render(ctx, sw); result = sw.ToString(); } return result; }

A dummy controller context class is also required for the type argument of the generic.

public class GenericController : Controller { }

With all that in place it is possible to render your email body with a few lines like so.

var viewModel = new MyViewModel();

string html = RenderViewToString("~/Views/Shared/MyEmailView.cshtml", viewModel); 

// generate and send email using service or SMTPClient

This method works well but only if you have an active HttpContext i.e running inside ASP.NET. If you try to run this code in a service or self hosted OWIN context the no active HttpContext exception will be thrown.

After a bit more digging I found that you can still use the empty controller solution but you need to also mock the HttpContext before creating the empty controller. This can be done by adding the following line either as part of your CreateController method or prior to calling it.

HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null));

There are a few limitations to this setup:

  • Your email razor view template still needs to be written with email clients in mind, so with inline styles and email friendly html.
  • Your view cannot have any inline code that relies on a real HttpContext, Controller or Route, for obvious reasons.