Render Razor View to string in ASP.NET Core
Asked Answered
N

8

34

I use RazorEngine for parsing of templates in my MVC 6 project like this:

Engine.Razor.RunCompile(File.ReadAllText(fullTemplateFilePath), templateName, null, model);

It works fine for the beta 6. It does not work after upgrading to beta 7 with the error:

MissingMethodException: Method not found: "Void Microsoft.AspNet.Razor.CodeGenerators.GeneratedClassContext.set_ResolveUrlMethodName(System.String)". in RazorEngine.Compilation.CompilerServiceBase.CreateHost(Type templateType, Type modelType, String className)

This is global.json:

{
  "projects": [ "src", "test" ],
  "sdk": {
    "version": "1.0.0-beta7",
    "runtime": "clr",
    "architecture": "x64"
  }
}

This is project.json:

...
"dependencies": {
    "EntityFramework.SqlServer": "7.0.0-beta7",
    "EntityFramework.Commands": "7.0.0-beta7",
    "Microsoft.AspNet.Mvc": "6.0.0-beta7",
    "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta7",
    "Microsoft.AspNet.Authentication.Cookies": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Facebook": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Google": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-beta7",
    "Microsoft.AspNet.Authentication.Twitter": "1.0.0-beta7",
    "Microsoft.AspNet.Diagnostics": "1.0.0-beta7",
    "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta7",
    "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta7",
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta7",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta7",
    "Microsoft.AspNet.StaticFiles": "1.0.0-beta7",
    "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.Abstractions": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.Json": "1.0.0-beta7",
    "Microsoft.Framework.Configuration.UserSecrets": "1.0.0-beta7",
    "Microsoft.Framework.Logging": "1.0.0-beta7",
    "Microsoft.Framework.Logging.Console": "1.0.0-beta7",
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta7",
    "RazorEngine": "4.2.2-beta1"
  },
...
  "frameworks": {
    "dnx451": { }
  },
...

My template is:

@model dynamic
@{
    Layout = null;
}

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Registration</title>
</head>
<body>
<p>
    Hello, @Model
</p>
</body>
</html>

Does anyone have similar problems? There is another way to parse templates in MVC 6?

Newbill answered 14/9, 2015 at 7:5 Comment(1)
I didn't read the question, but voting up for the answer :)Staves
R
29

UPDATE July, 2016

Working fine on the following versions 1.0.0, RC2


Who's targeting aspnetcore RC2, this snippet might help you:

  • Create a separate Service, so you can use it either if you are not in a controller context, e.g. from a command line or on a queue runner, etc ...
  • Register this service in your IoC container in the Startup class

https://gist.github.com/ahmad-moussawi/1643d703c11699a6a4046e57247b4d09

Usage

// using a Model
string html = view.Render("Emails/Test", new Product("Apple"));

// using a Dictionary<string, object>
var viewData = new Dictionary<string, object>();
viewData["Name"] = "123456";

string html = view.Render("Emails/Test", viewData);

Notes

Links in Razor are rendered as relative URL, so this will not work on external views (like emails, etc ...).

As for now am generating the link on the controller and pass it to the view through the ViewModel.

Credit

The source is extracted from (Thanks To @pholly): https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs)

Rumple answered 18/5, 2016 at 10:22 Comment(6)
Gist is exactly same code as github.com/aspnet/Entropy/blob/dev/samples/… which was created because of github.com/aspnet/Mvc/issues/3091Temporal
I'm having trouble getting this to work if I use a link taghelper in the view: <a asp-controller="Whatever" asp-action="DoSomething" asp-route-thingy="ThingyValue">link text</a>.Heartburn
@BenCollins, I am assuming, you are getting a wrong URL (relative URL), which doesn't work on external links like emails, is that right ?Rumple
@Ahmad no, the issue was the the link taghelper needed more context in order to work. The link taghelper depends on UrlHelper, and UrlHelper needs real routedata or it throws an exception. Fortunately for me I am doing this within an MVC app, so I can just use IHttpContextAccessor to build an ActionContext with real valuesHeartburn
Yes I think this was my problem too, cz I've needed to add an extension method AbsoluteAction to the IUrlHelper, happy that you found a solution:)Rumple
If anyone is having a trouble where you cannot find the views make sure that in project.json you have "copyToOutput": { "include": "Views" }Tegucigalpa
T
22

I found this thread which discusses it: https://github.com/aspnet/Mvc/issues/3091

Someone in the thread created a sample service here: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs

After trial and error I was able to trim the service down so it only needs a valid HttpContext and a ViewEngine and I added an overload that doesn't require a model. Views are relative to your application root (they don't have to live in a Views folder).

You will need to register the service in Startup.cs and also register HttpContextAccessor:

//Startup.cs ConfigureServices()
services.AddTransient<ViewRenderService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.IO;

namespace LibraryApi.Services
{
    public class ViewRenderService
    {
        IRazorViewEngine _viewEngine;
        IHttpContextAccessor _httpContextAccessor;

        public ViewRenderService(IRazorViewEngine viewEngine, IHttpContextAccessor httpContextAccessor)
        {
            _viewEngine = viewEngine;
            _httpContextAccessor = httpContextAccessor;
        }

        public string Render(string viewPath)
        {
            return Render(viewPath, string.Empty);
        }

        public string Render<TModel>(string viewPath, TModel model)
        {
            var viewEngineResult = _viewEngine.GetView("~/", viewPath, false);

            if (!viewEngineResult.Success)
            {
                throw new InvalidOperationException($"Couldn't find view {viewPath}");
            }

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            {
                var viewContext = new ViewContext();
                viewContext.HttpContext = _httpContextAccessor.HttpContext;
                viewContext.ViewData = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                { Model = model };
                viewContext.Writer = output;

                view.RenderAsync(viewContext).GetAwaiter().GetResult();

                return output.ToString();
            }
        }
    }
}

Example usage:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using LibraryApi.Services;
using System.Dynamic;

namespace LibraryApi.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        ILogger<ValuesController> _logger;
        ViewRenderService _viewRender;
        public ValuesController(ILogger<ValuesController> logger, ViewRenderService viewRender)
        {
            _logger = logger;
            _viewRender = viewRender;
        }

        // GET api/values
        [HttpGet]
        public string Get()
        {
            //ViewModel is of type dynamic - just for testing
            dynamic x = new ExpandoObject();
            x.Test = "Yes";
            var viewWithViewModel = _viewRender.Render("eNotify/Confirm.cshtml", x);
            var viewWithoutViewModel = _viewRender.Render("MyFeature/Test.cshtml");
            return viewWithViewModel + viewWithoutViewModel;
        }
    }
}
Temporal answered 8/9, 2016 at 21:2 Comment(8)
Thank You - Works great with ASP.NET Core 1.0.1. Love the general/service approach :)Bluestone
Thanks @pholly, this works great with Core 1.1 as well. Its a really nice solution for the problem. Any thoughts on what it would take for _ViewImports.cshtml, _ViewStart.cshtml and _Layout.cshtml to be taken into account, including the RenderBody and RenderSection commands used in _Layout.cshtml? I tried placing them in the same folder as the view being loaded, but they weren't used, at least based on the resulting output.Quadrivalent
@TommyBaggett I based the code on this: github.com/aspnet/Entropy/blob/dev/samples/… so I can't take the credit :) I didn't realize _Layout.cshtml and such wouldn't be used - perhaps an ActionContext is needed after all for those to be loaded? Try the service I linked to and see if it works. It uses an ActionContext.Temporal
Thanks @pholly, I gave that one a try and had issues with it finding my views. I gave the RazorLight engine a try (github.com/toddams/RazorLight) as suggested in another answer below. Its working great.Quadrivalent
With ASP.NET Core 1.1.0 I was having an issue that went away when I started using the version here: github.com/aspnet/Entropy/blob/dev/samples/…. Before adopting the approach in the sample, the call stack for my error was very similar to this one: github.com/aspnet/Mvc/issues/5505Bluestone
Thank you Richard Logwood. I had the same issue - trying to use my views as email content - and went ahead to use the example at your link.Attenuation
Is there any reason why you've registered IHttpContextAccessor as a singleton?Drice
Is there any way to get this to render javascript on the client? For example this does not render: <div id="Test"></div> and document.getElementById("Test").innerHTML = "whatever". The javascript never executes.Marv
M
15

In the past, I’ve used the RazorEngine inside a Class Library because my goal was to render templates from within this Class Library.

From my understanding, you seem to be inside an MVC 6.0 project so why not use a RenderPartialViewToString() method without having to add the dependency on the RazorEngine?

Keep in mind, I'm only asking because I'm curious.

For example purposes, from within VS2015, I created a new ASP.NET Web Application and selected the Web Application template from the ASP.NET 5 Preview Templates.

Inside the ViewModels folder, I created a PersonViewModel:

public class PersonViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName
    {
        get
        {
            return string.Format("{0} {1}", this.FirstName, this.LastName);
        }
    } 
}

I then created a new BaseController and added a RenderPartialViewToString() method:

public string RenderPartialViewToString(string viewName, object model)
{
    if (string.IsNullOrEmpty(viewName))
        viewName = ActionContext.ActionDescriptor.Name;

    ViewData.Model = model;

    using (StringWriter sw = new StringWriter())
    {
        var engine = Resolver.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
        ViewEngineResult viewResult = engine.FindPartialView(ActionContext, viewName);

        ViewContext viewContext = new ViewContext(ActionContext, viewResult.View, ViewData, TempData, sw,new HtmlHelperOptions());

        var t = viewResult.View.RenderAsync(viewContext);
        t.Wait();

        return sw.GetStringBuilder().ToString();
    }
}

Credit goes to @DavidG for his method

Inside the Views-->Shared folder, I created a new Templates folder in which I’ve added a simple RegistrationTemplate.cshtml View strongly typed to my PersonViewModel like so:

@model MyWebProject.ViewModels.PersonViewModel
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Registration</title>
</head>
<body>
    <p>
        Hello, @Model.FullName
    </p>
</body>
</html>

The last step is to make my Controller inherit from my BaseController

public class MyController : BaseController

And create something like:

public IActionResult Index()
{
    var model = new PersonViewModel();
    model.FirstName = "Frank";
    model.LastName = "Underwood";
    var emailbody = base.RenderPartialViewToString("Templates/RegistrationTemplate", model);

    return View();
}

Of course, the example above is useless since I do nothing with the variable emailbody but the idea is to show how it’s used.

At this point, I could've(for example), invoke an EmailService and pass the emailbody:

_emailService.SendEmailAsync("[email protected]", "registration", emailbody);

I'm not sure if this is suitable alternative for your current task.

Marketing answered 15/9, 2015 at 3:0 Comment(2)
How to do the same inside the Class Library? You do not have a ActionContext there...Callison
The example above shows how to render a View to String. The example lives inside an asp.net MVC application where all the dependencies are already present. If you plan on using something similar but within a Class Library, the above approach may not be the best one as you won’t have all the necessary dependencies and what not...For that, you might want to try a different approach like this one: strathweb.com/2016/01/…Marketing
C
13

Today I've finished with my library that can solve your problem. You can use it out of ASP.NET as it has no dependencies on it

Example:

string content = "Hello @Model.Name. Welcome to @Model.Title repository";

var model = new
{
  Name = "John Doe",
  Title = "RazorLight"
};

var engine = new RazorLightEngine();
string result = engine.ParseString(content, model);

//Output: Hello John Doe, Welcome to RazorLight repository

More: https://github.com/toddams/RazorLight

Convoy answered 24/7, 2016 at 11:29 Comment(4)
Thanks for making your RazorLight library available. Some of the other solutions offered here work as long as you're parsing a single view file. I switched to your library when I needed support for _ViewStart and _Layout files being taken into account. Your lib is working great for my needs.Quadrivalent
I had a hard time getting other solutions to work. But this worked great!!Khz
@toddams can you help me with issue I opened, we are trying to use the library for genrating dynamic templates from dynamic models issue https://github.com/toddams/RazorLight/issues/158Tempa
Great solution. Cheers.Mani
K
4

To improve on @vlince answer (that wasn't working out of the box for me), here is what I did :

1- Create a base controller that your other controller will inherit

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.IO;

namespace YourNameSpace
{
    public class BaseController : Controller
    {
        protected ICompositeViewEngine viewEngine;

        public BaseController(ICompositeViewEngine viewEngine)
        {
            this.viewEngine = viewEngine;
        }

        protected string RenderViewAsString(object model, string viewName = null)
        {
            viewName = viewName ?? ControllerContext.ActionDescriptor.ActionName;
            ViewData.Model = model;

            using (StringWriter sw = new StringWriter())
            {
                IView view = viewEngine.FindView(ControllerContext, viewName, true).View;
                ViewContext viewContext = new ViewContext(ControllerContext, view, ViewData, TempData, sw, new HtmlHelperOptions());

                view.RenderAsync(viewContext).Wait();

                return sw.GetStringBuilder().ToString();
            }
        }
    }
}

2- Inherit the base controller and call the method

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;

namespace YourNameSpace
{
    public class YourController : BaseController
    {
        public YourController(ICompositeViewEngine viewEngine) : base(viewEngine) { }

        public string Index(int? id)
        {
            var model = new MyModel { Name = "My Name" };

            return RenderViewAsString(model);
        }
    }
}
Kerekes answered 3/8, 2016 at 1:19 Comment(2)
Thank You - Works great with ASP.NET Core 1.0.1Bluestone
Should TutorialController actually be YourController in the example? Otherwise, thanks a bunch for this idea. I had forgotten this technique, more or less, from my old ASP.NET form days with the use of BaseForm :)Sharecropper
C
3

ResolveUrlMethodName was removed. Therefore in your CreateHost here you're trying to set a property that doesn't exist :).

We decided to move ~/ handling from core Razor into a TagHelper implemented in the Microsoft.AspNet.Mvc.Razor assembly. Here's the commit to the bits that removed the method.

Hopefully this helps.

Cns answered 14/9, 2015 at 18:12 Comment(1)
Thanks for the detailed explanation! I'll have a view of it in the future.Newbill
L
2

Extension method for converting partial views to string response.

public static class PartialViewToString
{
    public static async Task<string> ToString(this PartialViewResult partialView, ActionContext actionContext)
    {
        using(var writer = new StringWriter())
        {
            var services = actionContext.HttpContext.RequestServices;
            var executor = services.GetRequiredService<PartialViewResultExecutor>();
            var view = executor.FindView(actionContext, partialView).View;
            var viewContext = new ViewContext(actionContext, view, partialView.ViewData, partialView.TempData, writer, new HtmlHelperOptions());
            await view.RenderAsync(viewContext);
            return writer.ToString();
        }
    }
}

Usage in your controller actions.

public async Task<IActionResult> Index()
{
    return await PartialView().ToString(ControllerContext)
}

.NET 5 Implementation

public static async Task<string> ViewToString(this PartialViewResult partialView, Controller controller)
    {
        using (var writer = new StringWriter())
        {
            var services = controller.ControllerContext.HttpContext.RequestServices;
            var viewEngine = services.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
            var viewName = partialView.ViewName ?? controller.ControllerContext.ActionDescriptor.ActionName;
            var view = viewEngine.FindView(controller.ControllerContext, viewName, false).View;
            var viewContext = new ViewContext(controller.ControllerContext, view, partialView.ViewData, partialView.TempData, writer, new HtmlHelperOptions());
            await view.RenderAsync(viewContext);
            return writer.ToString();
        }
    }
Loya answered 6/7, 2017 at 4:59 Comment(1)
this doesn't seem to work on .NET 5 anymore. I couldn't figure out how to get the PartialViewResultExecutor to be registered. :( No service for type 'Microsoft.AspNetCore.Mvc.ViewFeatures.PartialViewResultExecutor' has been registered.Broadminded
V
2

An alternative solution using just ASP.NET Core, no external libraries, and no reflection can be found here: https://weblogs.asp.net/ricardoperes/getting-html-for-a-viewresult-in-asp-net-core. It just requires a ViewResult and an HttpContext.

The idea is to pick up a ViewResult and call some method, say, ToHtml, to get the rendered output. This method can look like this:

public static class ViewResultExtensions {
    public static string ToHtml(this ViewResult result, HttpContext httpContext) {            
        var feature = httpContext.Features.Get<IRoutingFeature>();
        var routeData = feature.RouteData;
        var viewName = result.ViewName ?? routeData.Values["action"] as string;
        var actionContext = new ActionContext(httpContext, routeData, new ControllerActionDescriptor());
        var options = httpContext.RequestServices.GetRequiredService<IOptions<MvcViewOptions>>();
        var htmlHelperOptions = options.Value.HtmlHelperOptions;
        var viewEngineResult = result.ViewEngine?.FindView(actionContext, viewName, true) ?? options.Value.ViewEngines.Select(x => x.FindView(actionContext, viewName, true)).FirstOrDefault(x => x != null);
        var view = viewEngineResult.View;
        var builder = new StringBuilder();

        using (var output = new StringWriter(builder)) {
            var viewContext = new ViewContext(actionContext, view, result.ViewData, result.TempData, output, htmlHelperOptions);

            view
                .RenderAsync(viewContext)
                .GetAwaiter()
                .GetResult();
        }
        return builder.ToString();
    }
}

To use it, just do:

var view = this.View(“ViewName”);
var html = view.ToHtml();
Vitrain answered 8/11, 2017 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.