Render Razor View to string in ASP.NET Core

46,304

Solution 1

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)

Solution 2

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;
        }
    }
}

Solution 3

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.

Solution 4

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

Solution 5

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);
        }
    }
}
Share:
46,304
hcp
Author by

hcp

Updated on April 09, 2021

Comments

  • hcp
    hcp about 3 years

    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?