Rendering Emails with RazorViewEngine in .NET Core 2.0

Rendering Emails with RazorViewEngine in .NET Core 2.0

In this post I’m going to cover how to use the RazorViewEngine to render Views, and get the string content so it can be used as an email template.
As always, the code for this example can be found on Github here.

This has been done in .NET Core 2.0, and I’ve created this on my Mac – though being .NET Core it works equally well on Windows (tested on my Surface Pro), and in theory should on Linux too.

To start with I created a Web API and Console project using the dotnet cli. And given this was done through the cli, I then created a solution and added the two projects to it the same way.
In the code for the API I modified the Program class to set a specific port – 5020. This is just so that it launches on the same port when I run the code, so my Console app knows where to send requests.
It’s worth noting here that if you’re running this through Visual Studio you will have to set the port on the project setting as Visual Studio overrides the setting.

The first thing to do is set up a controller for us to talk to. By default the API comes with a ‘Values’ controller, so I’ve renamed that to ‘Email’, removed the boilerplate code, and added a simple Get method that, for now, just returns a hard coded string:

I’ve then set up the Console to send a request to this endpoint, printing out the result. This is just so we can easily see what’s being output:

Now if you run both applications, you can see simply that “hello” gets printed to the console whenever we press enter.
The next step is to start setting up our API for the rendering. I’ve created a ‘Templating’ folder in the API, and a subfolder called ‘Emails’. In the Emails folder I’ve added one .cshtml file called HelloEmail.cshtml which is blank for now.
Create a class under the ‘Templating’ folder called RazorViewToStringRenderer. The purpose of this class is going to be to find an IView through the RazorViewEngine, and render that by calling RenderAsync.
The RenderAsync method takes in a ViewContext parameter. So the first thing we need to do is create the ViewContext – which means we need the parameters.
The first parameter is an ActionContext. I’ve added a private method to generate this which simply sets up a HttpContext, and uses that to create the ActionContext:

As you can see I’ve used a field called _serviceProvider, which is an implementation of IServiceProvider, that we don’t have yet, so let’s add that at the top of the class. We’re also going to make use of two other fields – ITempDataProvider and IRazorViewEngine:

The TempDataProvider is needed for the ViewContext later, and the RazorViewEngine is what will help find and render our Views.
These fields will need setting, so let’s create a constructor for this class passing in the three interfaces, and setting the fields:

Now we need a method that’s going to make use of GetActionContext, as well as handling the rest of the operation, so add an async method (RenderAsync, remember!) that takes in the name of the View we want, and the type of the model for the View – TModel so this can be used for any and all Views.
In here we can call to create our ActionContext, and make use of the defined RazorViewEngine, and the view name parameter, to locate our View. After the call to locate the View, I’ve checked the Success property to check the View was actually found. The method should look like this for now:

As you can see we get our ActionContext, and use that and the passed in name variable to locate the View through the RazorViewEngine. If successful we get the View from the result, otherwise we throw an exception – obviously what you do in this scenario is up to you!

Now we have the View, we need to create the ViewContext. As mentioned, the first parameter is the ActionContext that we already have. The remaining parameters are the View, a ViewDataDictionary, a TempDataDictionary, a TextWriter, and HtmlHelperOptions.
So to create the ViewContext we pass in the ActionContext and View objets we already have, then we can create the remaining parameters – we instantiate a ViewDataDictionary of type TModel, with new instances of both parameters, and set the Model property to our passed in model.
Next is the TempDataDictionary which we create by passing in the HttpContext of our ActionContext, and our _tempDataProvider field.
For the TextWriter, I’ve added a using statement for a StringWriter, and placed the creation of the ViewContext in there – then I can used the defined StringWriter for the parameter.
Lastly, pass in a new instance of HtmlHelperOptions:

Now that we have both our View, and the ViewContext, we can simply call the RenderAsync method, and get the string output. Inside the using statement, after the creation of the ViewContext, we want to add the following lines:

This method will now render our View with the provided model, and return us the string representation of that!
We’ve passed in three parameters to this class, and we don’t want to worry about handling the setup of those objects, so we can use dependency injection to do the heavy lifting.
We want to add an interface for this class, so I created an IViewToStrinRenderer interface, and had the class implement that. The interface simply defines the one method we’ve already implemented:

Now we can configure the startup to register this interface and implementation in the AddServices method:

.NET will do the rest for us!
Now we need the controller we previously set up to make use of this class. I’ve added a constructor to the EmailController class passing in IViewToStringRenderer, and setting this to a private field on the class. Once again, .NET handles injecting this into the controller, which should now start with this:

In order to test this, we’re going to need our templates to do something.
I’ve created a simple HelloEmailModel class, which just contains a Name property so we can see our model binding working. Then for the HelloEmail view, I’ve just set the model, and added a binding to the Name property:

Back in the EmailController, I’m going to hard code in some values, as it’s not important for this demo.
I’ve created an instance of the Model, and set my name. Then I’ve added a try-catch block, which makes use of the RazorViewToStringRenderer to try and make use of our HelloEmail template.
If an exception is thrown (which I added earlier if the view wasn’t found) we return an error message instead:

Great, let’s give it a run and see what happens…

Oh no! We’ve got an error instead.
Error: One or more errors occurred. (Couldn’t find view ‘HelloEmail’)

So RazorViewEngine can’t find the correct .cshtml file, let’s put a breakpoint in and see what’s happened.
We can see that the result’s Success indicator is false. Because of this, there is another property available to us – SearchedLocations. This tells us where the engine has tried to locate our view:

We can see that it’s searched the conventional folders for the templates, but that’s not where I’ve placed them. You could place the template there, but I like to keep things separated more – especially if you intend for your API to actually have some Views.
So now we need to tell the engine to look in the correct location. To do this, we need to add a ViewLocationExpander.
Under the Templating folder create a new class called ViewLocationExpander. This class will implement the IViewLocationExpander.
Implementing the interface gives us two methods we need to populate – ExpandViewLocations and PopulateValues.
I’ve added a list of strings which gets populated in the constructor – this is populated by getting the current directory, and finding all ‘Emails’ folders in there.
Then in the ExpandViewLocations we Union our list, with the passed in list of ViewLocations – if you don’t want to search the default locations, just return your own list.
For PopulateValues we’re just adding a customviewlocation value to the context, for the name of our expander.

In order to make use of the expander, we need to configure the RazorViewEngine to make use of it.
In the Startup, we need to configure the RazorViewEngineOptions to add our expander to the ViewLocationExpanders on the options.

If you run it now you’ll see…. An error still!
Putting a break point in again will show that we have indeed searched an extra location, but it doesn’t look quite right:

For one, that’s an absolute path, which we don’t want as RazorViewEngine works with absolute paths. And you’ll also notice that it’s searched the folder, but not for the view we’re after. We can make a minor change to the ViewLocationExpander to fix this.
We want to remove the root path from each found location, and also append the file name, which can be done with a couple of select statements:

If we run this now, we’ll see that the email has been rendered with our model, hooray!

Let’s make a useful addition – if you have multiple different emails you’ll be managing, you don’t want to be maintaining all the layouts separately. It’s nice to have a consistent theme applied for you – so let’s add a layout file.
Under the Emails folder, add a _Layout.cshtml file.
I’m not going into the details of formatting etc, just enough to show working with layouts works.
In the layout I’m just setting the structure of the page, adding a title, and rendering our body content:

Now if you run the app, you can see our layout is rendered as well as our View. The same works for rendering sections for scripts, content etc – just be careful about what your email client will allow!

There are two small things we want to do now – to make sure things run as expected when we deploy.
The first, is to ensure that the email templates are copied to the output directory – they can be found in the source folder now, but they need to be present when you publish and deploy.
Add the following to your project file:

The second is to improve how we find the templates. The current idea works well enough, but what if you run the project from outside of it’s root folder? If you do so, and have another project containing views present, you can run into rendering issues due to confusion between views with the same name, or with different layout files being found.
To fix this, we pass in the content root path of the hosting environment, and filter out files that don’t match. Inject IHostingEnvironment into your startup, and store the content root in a field, then pass this to the expander. In the expander constructor, we want to only find files that contain that path:

Now you can use the RazorViewEngine to render a View, and get the string content to be used for an email.

One last thing to mention is a potential issue when running integration tests. If you were to add a test using the renderer, you may see a lot of confusing errors about missing references in the View files. A workaround that worked for me is mentioned in this Github issue:
It’s just a case of creating one file, and adding a few lines to your project file. Making those changes and the errors went away for me 🙂

As always, please comment, raise an issue on Github, or otherwise get in touch if you see any problems or improvements!