Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Render a Razor View containing a URL to a string in ASP.NET Core

I have an Emailer class I am using via Dependency Injection to send emails which gets the contents of a View to send in an email. The process I have works great UNLESS the view contains a call to the underlying URL helper, such as using an A tag like this:

<a asp-controller="Project" asp-action="List">Open</a>

Here is the code I am using to render a view into a string:

private string renderViewAsString<TModel>(string folder, string viewName, TModel model)
{
  var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
  var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
  var viewEngineResult = _viewEngine.FindView(actionContext, folder + "/" + viewName, false);
  var view = viewEngineResult.View;

  var viewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary());
  viewData.Model = model;

  var tempData = new TempDataDictionary(httpContext, _tempDataProvider);

  using (var output = new StringWriter())
  {
    var viewContext = new ViewContext(actionContext, view, viewData, tempData, output, new HtmlHelperOptions());        
    var task = view.RenderAsync(viewContext);
    task.Wait();

    return output.ToString();
  }
}

_serviceProvider is of type IServiceProvider and _viewEngine is of type IRazorViewEngine which are both injected in the constructor.

If it references the URL helper it produces this exception at the task.Wait() line:

Index was out of range. Must be non-negative and less than the size of the collection.
Parameter name: index

with this as the call stack:

at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
at System.Collections.Generic.List`1.get_Item(Int32 index)
at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.get_Router()
at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.GetVirtualPathData(String routeName, RouteValueDictionary values)
at Microsoft.AspNetCore.Mvc.Routing.UrlHelper.Action(UrlActionContext actionContext)
at Microsoft.AspNetCore.Mvc.UrlHelperExtensions.Action(IUrlHelper helper, String action, String controller, Object values, String protocol, String host, String fragment)
at Microsoft.AspNetCore.Mvc.ViewFeatures.DefaultHtmlGenerator.GenerateActionLink(ViewContext viewContext, String linkText, String actionName, String controllerName, String protocol, String hostname, String fragment, Object routeValues, Object htmlAttributes)
at Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper.Process(TagHelperContext context, TagHelperOutput output)
at Microsoft.AspNetCore.Razor.TagHelpers.TagHelper.ProcessAsync(TagHelperContext context, TagHelperOutput output)
at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.<RunAsync>d__0.MoveNext()

How do I get around this without having to resort to hard-coding the A element or email contents?

like image 516
Rono Avatar asked Dec 01 '22 16:12

Rono


2 Answers

I was able to get it to work. The call stack mentioned not finding a router, so it was a matter of providing it:

First I added this as a DI object in the constructor parameters:

IHttpContextAccessor accessor

And this in the constructor:

_context = accessor.HttpContext;

Then I changed the function to this:

private string renderViewAsString<TModel>(string folder, string viewName, TModel model)
{
  var actionContext = new ActionContext(_context, new RouteData(), new ActionDescriptor());
  var viewEngineResult = _viewEngine.FindView(actionContext, folder + "/" + viewName, false);
  var view = viewEngineResult.View;

  var viewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary());
  viewData.Model = model;

  var tempData = new TempDataDictionary(_context, _tempDataProvider);

  using (var output = new StringWriter())
  {
    var viewContext = new ViewContext(actionContext, view, viewData, tempData, output, new HtmlHelperOptions());
    viewContext.RouteData = _context.GetRouteData();   //set route data here

    var task = view.RenderAsync(viewContext);
    task.Wait();

    return output.ToString();
  }
}
like image 85
Rono Avatar answered Dec 15 '22 20:12

Rono


This is the one I use in ASP.NET Core 2.0

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;

namespace Website
{
    public class RazorViewToStringRenderer
    {
        private readonly IHttpContextAccessor accessor;
        private readonly IRazorViewEngine viewEngine;
        private readonly IServiceProvider serviceProvider;
        private readonly ITempDataProvider tempDataProvider;

        public RazorViewToStringRenderer(
            IHttpContextAccessor accessor, 
            IRazorViewEngine viewEngine, 
            IServiceProvider serviceProvider, 
            ITempDataProvider tempDataProvider)
        {
            this.accessor = accessor;
            this.viewEngine = viewEngine;
            this.serviceProvider = serviceProvider;
            this.tempDataProvider = tempDataProvider;
        }

        public string RenderViewToString<TModel>(string viewLocation, TModel model)
        {
            HttpContext httpContext = accessor.HttpContext;

            httpContext.RequestServices = serviceProvider;

            ActionContext actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

            IView view = FindView(actionContext, viewLocation);

            using (StringWriter stringWriter = new StringWriter())
            {
                ViewDataDictionary<TModel> viewDataDictionary = new ViewDataDictionary<TModel>(
                    new EmptyModelMetadataProvider(), 
                    new ModelStateDictionary());

                viewDataDictionary.Model = model;

                TempDataDictionary tempDataDictionary = new TempDataDictionary(
                    actionContext.HttpContext,
                    tempDataProvider);

                HtmlHelperOptions htmlHelperOptions = new HtmlHelperOptions();

                ViewContext viewContext = new ViewContext(
                    actionContext,
                    view,
                    viewDataDictionary,
                    tempDataDictionary,
                    stringWriter,
                    htmlHelperOptions);

                viewContext.RouteData = accessor.HttpContext.GetRouteData();

                view.RenderAsync(viewContext).Wait();

                return stringWriter.ToString();
            }
        }

        private IView FindView(ActionContext actionContext, string viewLocation)
        {
            ViewEngineResult getViewResult = viewEngine.GetView(null,  viewLocation,  true);

            if (getViewResult.Success)
            {
                return getViewResult.View;
            }

            ViewEngineResult findViewResult = viewEngine.FindView(actionContext, viewLocation, true);

            if (findViewResult.Success)
            {
                return findViewResult.View;
            }

            IEnumerable<string> searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);

            string message = string.Join(
                Environment.NewLine,
                new[] { $"Unable to find view '{viewLocation}'. The following locations were searched:" }.Concat(searchedLocations)); ;

            throw new Exception(message);
        }
    }
}

Remember in Startup.cs -> public void ConfigureServices(IServiceCollection serviceCollection) to add this

serviceCollection.AddSingleton<RazorViewToStringRenderer>();
like image 45
Thomas Kold Avatar answered Dec 15 '22 20:12

Thomas Kold