Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • JavaScript • Angular
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Back to Basics: Rendering Razor Views to String in ASP.NET Core


:P
On this page:

Many many moons ago I wrote a post that described how to render Razor views to string in classic ASP.NET MVC. It turns out that rendering views to string must be pretty common, because that post ended up being one of the most popular ones on the site for quite some time.

The most common scenario I have for 'template driven' non-Web text output is for emails of all sorts. Email confirmations and account verifications, order or receipt confirmations, status updates or scheduler notifications - all of which require merged text output both within and sometimes outside of Web application. On other occasions I also need to capture the output from certain views for logging purposes.

While it's certainly possible to create HTML (or non HTML) text output for sending emails or reporting in code, it's a heck of a lot easier and more maintainable to have that code in an easily editable text template, that can be easily edited and updated, potentially without requiring a recompile.

How to capture Razor Output

The good news is that capturing Razor output to string within the context of an ASP.NET Core application is relatively easy to do. But the caveat is that this requires access to a ControllerContext and an active request, so the solution I describe here effectively only works in the context of an active request in an ASP.NET Core application.

Without much further ado, here's the self-contained code to render a view to string:

public static async Task<string> RenderViewToStringAsync(
    string viewName, object model,
    ControllerContext controllerContext,
    bool isPartial = false)
{
    var actionContext = controllerContext as ActionContext;
    
    var serviceProvider = controllerContext.HttpContext.RequestServices;
    var razorViewEngine = serviceProvider.GetService(typeof(IRazorViewEngine)) as IRazorViewEngine;
    var tempDataProvider = serviceProvider.GetService(typeof(ITempDataProvider)) as ITempDataProvider;

    using (var sw = new StringWriter())
    {
        var viewResult = razorViewEngine.FindView(actionContext, viewName, !isPartial);

        if (viewResult?.View == null)
            throw new ArgumentException($"{viewName} does not match any available view");
        
        var viewDictionary =
            new ViewDataDictionary(new EmptyModelMetadataProvider(),
                new ModelStateDictionary())
            { Model = model };

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

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

code on GitHub

Short and sweet.

Using the ViewRenderer

The code is surprisingly short, although finding the right combination of actions here is not exactly easy to discover. I had a bit of help from this StackOverflow answer which provides the base concepts. I used that code and distilled down into a more compact single methods that's more convenient to use without explicit service configuration.

To use the method is as simple as:

// inside of a Controller method
string confirmation = await ViewRenderer.RenderViewToStringAsync("EmailConfirmation", model, ControllerContext);

EmailConfirmation here is the name of a view in the default View folder for the current controller's ActionContext which is passed in as the 3rd parameter. So in my case I'm rendering a page called EmailConfirmation in my ShoppingCart controller and it finds the view in that same ShoppingCart/OrderConfirmation.cshtml file.

View Path Resolution

The way the ViewPath is resolved unfortunately is not very straight forward and doesn't quite work the same as it does for standard Views. So you can't use ~/ or even absolute paths. It appears only the following paths will resolve:

  • Controller's default View folder
  • Shared folder
  • View relative paths ie ../SomeOtherViewFolder/MyView

For Razor Pages, the ActionContext is the single page running, and in that scenario the current page folder is used as the base.

How it works

As said the code is surprisingly short given the functionality it provides for dynamic invocation and rendering of a view.

The basic steps are:

  • Finding the View
  • Configuring the View's context
  • Executing the view and capturing output into a TextWriter

The rendering method takes a view path, model and a controller context to retrieve the view using the active RazorViewEngine of the application. This is the globally registered ViewEngine that has all the configuration applied so it can find views using standard view naming formats.

The view name has to be specified without .cshtml and follows the rules described in the side bar above. If the view is in a non-standard location you might have to experiment a bit to have the view engine find it.

The key item passed in is the ControllerContext which ties this functionality to the ASP.NET Core libraries and an actively running request. Using the context it's possible to get access to the other important dependencies such as the HttpContext, the ActionContext and - via the Service Provider - the RazorViewEngine and TempDataProvider all of which are required as part of the processing. In the code above the ServiceProvider is explicitly used to retrieve the required dependencies through DI.

The first thing that happens is retrieving the View from the RazorViewEngine. The view engine retrieves the View to process, assigns the ActionContext and sets whether it's a partial view or a top level view. Specifying top level automatically pulls in a Layout page if specified for example while a partial view processes just the current View.

Next a ViewContext is created which is then passed to the View.RenderAsync(viewContext) to actually run the view and produce the result which is written into the the passed in TextWriter.

Rendering Views to String Considerations

Rendering is easy, but if you need to render to string and then use that content for use externally to the application, such as in an email there are some additional considerations you need to take into account.

Specifically, for rendering scenarios like output to email, you have to be careful about your view dependencies.

For example, in my Order Confirmation email example it may seem tempting to use the main order confirmation Web page used on the site to confirm a completed order and just capture that to string with the View renderer. Here's what this page looks like:

While that page works on the Web, it can't be used as is for an email, as there are additional bits in the HTML output like the site chrome, dependencies on Bootstrap and FontAwesome, additional images etc.

Instead the email confirmation is a completely separate View that is a stripped down version of the full page (displayed here in the PaperClip local SMTP server:

The string rendered view looks very similar, but you can see it's a bit stripped down. There's no application chrome, no FontAwesome icons and the styling behind the scenes doesn't use Bootstrap and instead replaces the used styles with all inline CSS.

The self-contained view explicitly removes the Site Layout page and explicitly adds all needed CSS inline:

<!--  EmailConfirmation.cshtml -->
@model OrderProfileViewModel
@{
    Layout = null;  // explicitly no layout page here!
    
    var config = wsApp.Configuration;
    string title =  "Order Confirmation - " + config.ApplicationName;
    var invoice = Model.Invoice.Invoice;
    var customer = Model.Customer;
}
<!DOCTYPE html>
<html>
<head>
    <title>@(title)</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <style>
        html, body {
            font-size: 16px;
            font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Liberation Sans",sans-serif;
            line-height: 1.45;
        }
        .page-header-text {
            font-size: 1.52em;    
            color:  #0092d0;
            padding-bottom: 0.35em;
            margin-bottom: 0.6em;
            border-bottom: 1px solid #e1e1e1;
        }

   	/* all needed styles inline here */ 
   	
    </style>
</head>        

Then any links or references that are embedded in the page are fixed up with the application's full URL base path:

<div class="d-flex" style="display: flex">
@if (!Model.NoItemImages) {
    <div style="max-height: 3em; max-width: 3em; min-width: 3em; margin-right: 1.7em; margin-top: 0.3em">
        <img src="@(wsApp.Configuration.ApplicationHomeUrl + 
                 "images/product-images/sm_" + item.ItemImage)" 
             style="max-height: 3em; max-width: 3em; display: block;"/>
    </div>
}
	<div>
    <a href="@(wsApp.Configuration.ApplicationHomeUrl + "product/" + item.Sku)"
       class="lineitem-description d-block"></a>
    	@item.Description
    </a>
...

ApplicationHomeUrl is an application configuration value that specifies the full site root base path which typically is something like https://store.west-wind.com/.

Note that external images in email clients may or may not render initially depending on whether the email client (like Gmail, Outlook.com etc.) fixes up the image links with proxies, or doesn't render the images (Outlook desktop) unless explicitly allowed. In this case the images are nice to have but not a critical part so if they don't render it's not a big deal.

In general for string views I try to:

  • Minimize or remove images
  • Inline all CSS styles
  • Explicitly make links absolute

Rarely is it possible to just reuse entire site views as is, so in most cases string rendered views are their customized views optimized for self-contained HTML output.

Summary

So, rendering HTML views to string can be quite useful in creating content that needs to be somewhat self contained for common tasks like emails. It's easy to do inside of an ASP.NET Core application and works for MVC Views or Razor Pages.

Although you might be tempted to reuse existing views for string rendering, for most scenarios that involve sharing the HTML output externally from the application, you likely need to create separate views that are specifically set up to be self-contained. It's more work, but ultimately required for most scenarios.

I've found View string rendering useful in just about any application - it's one of those things that nobody thinks about until you end up needing to create HTML formatted emails or reports or otherwise sharing previously captured output.

Hopefully this View Rendering helper will be helpful to a few of you, given that its an easy drop-in component you can quickly plug into an existing application.

No running with scissors... uh, razors, folks!

Resources

this post created and published with the Markdown Monster Editor
Posted in ASP.NET Core  

The Voices of Reason


 

Joe Enos
June 21, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

I just wish it was easier to do it outside the context of a web request. There are times when I want to build HTML somewhere else, and have to use Mustache or Handlebars.

I've seen some people try to implement it, but there always seem to be disclaimers that it may or may not always work for whatever reasons.

It would have been awesome if Microsoft would have made Razor a standalone templating engine, and then plugged it into ASP.NET.


Rick Strahl
June 21, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

@Joe - I hear you and I agree. It's possible to do outside of ASP.NET in say a desktop app, but you then still end up pulling in the ASP.NET libraries to make it happen. And getting a ControllerContext that works as expected is a pain. There are a couple of projects out there that do this, but they don't look very solid.

I had a library for full framework, but there too it was a royal pain in the ass to get it to work and there are lots of limitations running outside of ASP.NET there too. I would love to do this for Core again, but I can't justify the work for that. I've resigned myself to use custom C# scripting using Handlebars like syntax in a small library instead:

and there are other libraries like Fluid (Liquid template language) that also uses a handlebars like syntax, but without the C# language structures.


roger
June 22, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core


Tobias Bartsch
June 22, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

I used a similar approach to render my email templates but without an active http request. I just create my own ActionContext like this. I don't know if it works in all circumstances, but it was perfectly adequate for my needs.

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

        var currentUri = new Uri("htpps://myDomain.tdl"); // Here you have to set your url if you want to use links in your email
        httpContext.Request.Scheme = currentUri.Scheme;
        httpContext.Request.Host = HostString.FromUriComponent(currentUri);
        
        return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
    }

Rick Strahl
June 22, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

@Tobias - I haven't tried this, but you still end up with dependency on the ServiceProvider that's been populated by the ASP.NET startup in order to work. I think without those dependencies many things likely won't work - especially the path and view folder resolutions.


Thomas Ardal
June 24, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

This is pretty much what I'm doing to render an invoice from a Razor view and then convert the HTML to a PDF file. I found that it's easier to just inject the IRazorViewEngine where I need it like this:

public MyController(IRazorViewEngine razorViewEngine)
{
    this.razorViewEngine = razorViewEngine;
}

public static async Task<string> RenderViewToStringAsync(
    string viewName, object model,
    ControllerContext controllerContext,
    bool isPartial = false)
{
    var actionContext = controllerContext as ActionContext;
    
    using (var sw = new StringWriter())
    {
        var viewResult = razorViewEngine.FindView(actionContext, viewName, !isPartial);
        ...
    }
}

Soundar
June 24, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

This looks great. But, for people looking for out of the box solution, they can try https://github.com/soundaranbu/RazorTemplating.

The major advantage is that it works outside of the MVC context. This means, they can use it in console applications as well.


Rick Strahl
June 24, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

@Thomas - your static method won't be able to see the RazorViewEngine so you'd have to pass it into that method.

@Soundar - yeah lots of people commenting in regards to standalone solutions which is great, but that's not what this post is about. It's specifically meant for the scenario where you can use what's already provided in the application without additional dependencies.


Thomas Ardal
June 25, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

@Rick Ahh, you're right. I copied your method signature to make the code similar to your example. Don't think I can change the comment, though.


Howard
June 28, 2022

# re: Back to Basics: Rendering Razor Views to String in ASP.NET Core

MVC views are a somewhat complicated way to render HTML strings. I used to use RazorEngine for this in old ASP.NET.

When we upgraded to Blazor/.NET 6 I looked for a Razor Component-based system and couldn't find one, so wrote my own: https://github.com/conficient/BlazorTemplater


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2022