Generate a HTML string from cshtml razor view using ASP.NET Core that can be used in the c# controller code
asp.net-core .net html c# viewtostring renderview render-view-as-string

Generate a HTML string from cshtml razor view using ASP.NET Core that can be used in the c# controller code

In several occasions, I needed a way to output the result of cshtml views as a string. Some day, I wanted a partial view that I could embed in a JSON response, another time it was to build an email template. For this, we need a feature that is mostly implemented with the Razor engine, but that is not publicly available (yet). In this article, you will discover how to build an efficient ViewToString renderer and get directly access to the HTML generated by a cshtml view.

ASP.NET Core engine provide a powerful engine to generate HTML, however it is not exposed on demand. We will discover how to build a class that will help us generating HTML string from a cshtml view.

There are few solutions on the web around, however as I met several issues with the existing options provided. I have ended up by investigating the ASP.NET Core framework structure and building my own class. It has more dependencies on the framework internals, but works like a charm 😃

Most of the implementations I found around had issues resolving View paths, this one use the same method as the framework to discover the views.

This class has mainly been inspired by the ViewResultExecutor class of the ASP.NET Core MVC framework and related classes.

Use cases for rendering an HTML string from a cshtml razor view

Yoy may think of many use-cases, but I use it quite often. The last opportunities I had to use this tool were:

  • to embed a HTML part of code in a JSON response, the html being complex and recursive, it was much easier to build it from a razor view
  • to build an email template from a cshtml razor view
  • to extract the HTML string from a razor view to build a PDF file of it
  • to generate dynamic javascript files

Implementation of the cshtml View To String Renderer Service

The final service class:

// (c) Jean Collas https://dotnetstories.com/blog/Generate-a-HTML-string-from-cshtml-razor-view-using-ASPNET-Core-that-can-be-used-in-the-c-controlle-7173969632
// Inspired by several web solutions and https://raw.githubusercontent.com/aspnet/Mvc/133dd964abb1c2a4167cf38faa38fe0319b7b931/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs

using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Options;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;


namespace MD.Bases
{

    public class ViewToStringRendererService: ViewExecutor
    {
        private readonly IActionContextAccessor _ActionContextAccessor;
        private ITempDataProvider _TempDataProvider;

        public ViewToStringRendererService(
            IActionContextAccessor actionContextAccessor,
            IOptions<MvcViewOptions> viewOptions,
            IHttpResponseStreamWriterFactory writerFactory,
            ICompositeViewEngine viewEngine,
            ITempDataDictionaryFactory tempDataFactory,
            DiagnosticSource diagnosticSource,
            IModelMetadataProvider modelMetadataProvider,
            ITempDataProvider tempDataProvider)
            : base(viewOptions, writerFactory, viewEngine, tempDataFactory, diagnosticSource, modelMetadataProvider)
        {
            _ActionContextAccessor = actionContextAccessor;
            _TempDataProvider = tempDataProvider;
        }

        public async Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model)
        {
            var context = GetActionContext();

            if (context == null) throw new ArgumentNullException(nameof(context));

            var result = new ViewResult()
            {
                ViewData = new ViewDataDictionary<TModel>(
                        metadataProvider: new EmptyModelMetadataProvider(),
                        modelState: new ModelStateDictionary())
                {
                    Model = model,
                },
                TempData = new TempDataDictionary(
                        context.HttpContext,
                        _TempDataProvider),
                ViewName = viewName,
            };

            var viewEngineResult = FindView(context, result);
            viewEngineResult.EnsureSuccessful(originalLocations: null);

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            {
                var viewContext = new ViewContext(
                    context,
                    view,
                    new ViewDataDictionary<TModel>(
                        metadataProvider: new EmptyModelMetadataProvider(),
                        modelState: new ModelStateDictionary())
                    {
                        Model = model
                    },
                    new TempDataDictionary(
                        context.HttpContext,
                        _TempDataProvider),
                    output,
                    new HtmlHelperOptions());

                await view.RenderAsync(viewContext);

                return output.ToString();
            }
        }
        private ActionContext GetActionContext()
        {
            return _ActionContextAccessor.ActionContext;
            //// Modified to get the global request context.
            //var httpContext = _httpContextAccessor.HttpContext;
            //if (httpContext == null)
            //{
            //    httpContext = new DefaultHttpContext();
            //    httpContext.RequestServices = _serviceProvider;
            //}
            //return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        }

        /// <summary>
        /// Attempts to find the <see cref="IView"/> associated with <paramref name="viewResult"/>.
        /// </summary>
        /// <param name="actionContext">The <see cref="ActionContext"/> associated with the current request.</param>
        /// <param name="viewResult">The <see cref="ViewResult"/>.</param>
        /// <returns>A <see cref="ViewEngineResult"/>.</returns>
        ViewEngineResult FindView(ActionContext actionContext, ViewResult viewResult)
        {
            if (actionContext == null)
            {
                throw new ArgumentNullException(nameof(actionContext));
            }

            if (viewResult == null)
            {
                throw new ArgumentNullException(nameof(viewResult));
            }

            var viewEngine = viewResult.ViewEngine ?? ViewEngine;

            var viewName = viewResult.ViewName ?? GetActionName(actionContext);

            var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
            var originalResult = result;
            if (!result.Success)
            {
                result = viewEngine.FindView(actionContext, viewName, isMainPage: true);
            }

            if (!result.Success)
            {
                if (originalResult.SearchedLocations.Any())
                {
                    if (result.SearchedLocations.Any())
                    {
                        // Return a new ViewEngineResult listing all searched locations.
                        var locations = new List<string>(originalResult.SearchedLocations);
                        locations.AddRange(result.SearchedLocations);
                        result = ViewEngineResult.NotFound(viewName, locations);
                    }
                    else
                    {
                        // GetView() searched locations but FindView() did not. Use first ViewEngineResult.
                        result = originalResult;
                    }
                }
            }

            if(!result.Success)
                throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", viewName));

            return result;
        }


        private const string ActionNameKey = "action";
        private static string GetActionName(ActionContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (!context.RouteData.Values.TryGetValue(ActionNameKey, out var routeValue))
            {
                return null;
            }

            var actionDescriptor = context.ActionDescriptor;
            string normalizedValue = null;
            if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out var value) &&
                !string.IsNullOrEmpty(value))
            {
                normalizedValue = value;
            }

            var stringRouteValue = routeValue?.ToString();
            if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
            {
                return normalizedValue;
            }

            return stringRouteValue;
        }
    }
	
    public static class VTSRExts
    {
		// Use this extension method to register the service in your `Startup.cs`, to avoid missing some service dependencies
        public static void AddViewToStringRendererService(this IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
            services.AddScoped<ViewToStringRendererService, ViewToStringRendererService>();
        }
    }
}

To use this class, you simply need to

  • register it on startup as services.AddViewToStringRendererService()
  • inject a dependency in your controller or service: ViewToStringRendererService viewToStringRenderer
  • call your service var html = await _ViewToStringRenderer.RenderViewToStringAsync("/Areas/MyFeature/Views/_MyHtmlToStringView.cshtml", viewModel)

It works well with absolute paths to views, and any other usual calls for the ASP.NET MVC framework should work too.

Enjoy 😃


Send
Please sign-in to comment
.X0001-01-01_00-00