We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Template based Emails - Fetch Rendered View HTML in ASP.NET Core using Razor

ASP.NET Core  • Posted 6 months ago

Email marketing is one of those important strategies applications use for user acquisition. A typical newsletter email with embedded images and formatted text layouts is internally a HTML document rendered over the email body. When we dig deeper, one can observe that these mails are generated by applying variable email content over a static template based HTML document; depending on the occasion and the data to be sent.

In this article, let's see how we can develop such an interesting and elegant email sending for an asp.net core application using the already existing features that the MVC library provides us with: the razor engine.

RazorEngine and Templating:

An MVC application developed over ASP.NET framework internally uses Razor to render html views and data over layout files during a request. Applications developed using ASP.NET can make use of an external library called RazorEngine which levarages the Razor engine capabilities of the MVC framework.

Although RazorEngine plugin has no direct support for ASP.NET Core, the aspnetcore framework still comes with the razor engine which provides helper methods which can by-produce the rendered HTML over the passed model data and return the plain HTML along.

To demonstrate how this works, let's take the following example of the ReadersApi in which we shall send the list of Readers available on the ReaderStore to some user over an email template.

Let's first add the template we want the email to be rendered with. To do this, Let's create a new folder Templates under the Views folder and add a new cshtml file called content.cshtml which contains the HTML layout for the email body. This cshtml file shall be rendered with the data we pass onto the Razor templating framework and fetch the plan HTML out of it.

"When working with Razor views, keep in mind that we need to put the template cshtml files only under Views folder"

@model List<ReadersApi.Providers.Reader>

<!DOCTYPE html>
<html>
    <head>
        <style>
            .no-border {
                border: 1px #dddddd;
            }
        </style>
    </head>
    <body>
        <div>
            <p>Hi,</p>
        </div>
        <div>
            <p>Please find the below list of readers currently available.</p>
        </div>
        <div>
            <table class="no-border">
                <tr>
                    <td>Id</td>
                    <td>User Name</td>
                    <td>Email Address</td>
                </tr>
                @foreach(var reader in @Model) {
                    <tr>
                        <td>@reader.Id</td>
                        <td>@reader.UserName</td>
                        <td>@reader.EmailAddress</td>
                    </tr>
                }
            </table>
        </div>
    </body>
</html>

The razor view file receives a model of type List and has the model iterated inside a table body as expected.

Next we shall have a generic TemplateHelper which helps us with the rendering and extracting stuff. We first create an interface ITemplateHelper that sets up the context for the implementation to occur.

namespace ReadersApi.Providers
{
    public interface ITemplateHelper
    {
        Task<string> GetTemplateHtmlAsStringAsync<T>(
                              string viewName, T model);
    }
}

We make sure that the method GetTemplateHtmlAsStringAsync() has a generic model type to be passed to it so that its not coupled with only a single type of model for usage.

Let's start working on the implementation for this interface, which will bear the template logic to produce plain HTML for an input Model and a View.

We make use of the below namespaces for the functionality.

using System;
using System.IO;
using System.Threading.Tasks;
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.ViewFeatures;
using Microsoft.AspNetCore.Routing;

namespace ReadersApi.Providers
{
    public class TemplateHelper : ITemplateHelper
    {
        public async Task<string> 
            GetTemplateHtmlAsStringAsync<T>(string viewName, T model)
        {
		    ......
        }
    }
}

First we being with setting up the httpContext on which the razor would work for the rendering. The RazorView works on the ViewContext which requires an instance of the HttpContext.

To generate HTML based on a View Template and Model, we implement the following steps:

  1. Setup a HttpContext, followed by an ActionContext
  2. Find and Set the View on which the rendering would happen
  3. Setup a ViewDataDictionary in which the Model to be rendered on shall be passed.
  4. Create a String writer stream and have a ViewContext be created basing on the actioncontext, viewdata and the stringwriter stream
  5. render the view over the context and have the output be written on the stringwriter
  6. return the string written and close the stream.

1. Setting up the ActionContext:

We make use of the IServiceProvider instance from the aspnetcore container by having it injected through the constructor.

    var httpContext = new DefaultHttpContext() { 
            RequestServices = _serviceProvider 
        };
    var actionContext = new ActionContext(
            httpContext, new RouteData(), new ActionDescriptor());

2. Setting up the View

We pass the viewName as a parameter to the method into the razorViewEngine.FindView() method which returns a ViewResult. The Razor View Engine searches for the matching views in all of the Views folder and returns a ViewResult object with information related to the fetched View.

    using (StringWriter sw = new StringWriter())
    {
        var viewResult = _razorViewEngine.FindView(
                actionContext, viewName, false);
        
        if (viewResult.View == null)
        {
            return string.Empty;
        }
        .......
    }

If the specified view is not available to the engine, we return with no further processing. Also observe that we use the razorViewEngine which is of type IRazorViewEngine injected via the constructor.

3. Setting up the ViewDataDictionary

We pass the model passed to the method as an argument to the ViewDataDictonary class, which passes the model data to the RazorView for rendering. This ViewDataDictionary is digested by the RazorView Engine over the passed View and a rendered HTML will be produced.

    var viewDataDictionary = new ViewDataDictionary(
        new EmptyModelMetadataProvider(), 
        new ModelStateDictionary()
    ) {
        Model = model
    };

4. Creating the ViewContext

We create a ViewContext instance based on all the parameters which we have created in earlier steps as arguments. Observe that the StringWriter is also passed as a constructor parameter to the class which then holds on the rendered plain HTML content for our use.

	var viewContext = new ViewContext(
        actionContext,
        viewResult.View,
        viewDataDictionary,
        new TempDataDictionary(
            actionContext.HttpContext, 
            _tempDataProvider),
        sw,
        new HtmlHelperOptions()
    );

5. Render the View and Return the HTML

We finally render the ViewData available in the ViewContext over the ViewResult created before.

    await viewResult.View.RenderAsync(viewContext);
    return sw.ToString();

Once the rendering is over, the HTML is available on the StringWriter which the method returns for our usage. On the Whole, our final implementation looks like below:

namespace ReadersApi.Providers
{
    public class TemplateHelper : ITemplateHelper
    {
        private IRazorViewEngine _razorViewEngine;
        private IServiceProvider _serviceProvider;
        private ITempDataProvider _tempDataProvider;

        public TemplateHelper(
            IRazorViewEngine engine,
            IServiceProvider serviceProvider,
            ITempDataProvider tempDataProvider)
        {
            this._razorViewEngine = engine;
            this._serviceProvider = serviceProvider;
            this._tempDataProvider = tempDataProvider;
        }

        public async Task<string> GetTemplateHtmlAsStringAsync<T>(
                string viewName, T model)
        {
            var httpContext = new DefaultHttpContext() { 
                    RequestServices = _serviceProvider };
            var actionContext = new ActionContext(
                    httpContext, new RouteData(), new ActionDescriptor());

            using (StringWriter sw = new StringWriter())
            {
                var viewResult = _razorViewEngine.FindView(
                        actionContext, viewName, false);

                if (viewResult.View == null)
                {
                    return string.Empty;
                }

                var viewDataDictionary = new ViewDataDictionary(
                    new EmptyModelMetadataProvider(),
                    new ModelStateDictionary()
                )
                {
                    Model = model
                };

                var viewContext = new ViewContext(
                    actionContext,
                    viewResult.View,
                    viewDataDictionary,
                    new TempDataDictionary(
                            actionContext.HttpContext, _tempDataProvider),
                    sw,
                    new HtmlHelperOptions()
                );

                await viewResult.View.RenderAsync(viewContext);

                return sw.ToString();
            }
        }
    }
}

To use this service, We add this to the service collection as a scoped service to be able to inject an instance whenever required.

namespace ReadersApi
{
    public class Startup
    {
        public void ConfigureServices(
                IServiceCollection services)
        {
	        ....
            services.AddScoped<ITemplateHelper, TemplateHelper>();
            ....
	}
	....
    }
}

To use the method, we can simply call:

var response = await _templateHelper
          .GetTemplateHtmlAsStringAsync<List<Reader>>(
                "Templates/Content", ReaderStore.Readers);

Where the template resides under the Templates folder of Views folder, and the model data is received from the ReaderStore. When we use this setup and print the rendered html on a View, it might look like this:

data/Admin/2020/5/razorviewengine.PNG

This way we can generate our own template content for use in emails by using the RazorEngine provided by AspNetCore. We can then use this template in sending out email for recipients using MimeKit and MailKit libraries via SMTP or use a third-party API services such as SendGrid for pushing mails to users.

You can check out the sample code we used in our example, under this git repository: https://github.com/referbruv/razorviewenginesample.git

.net core razor email template razor email template .net core asp.net core email template razor razor email template net core asp.net core render razor view to string razorengine .net core razor engine razor email template razorengine .net core example