By Hasan A Yousef


2016-12-01 13:49:25 8 Comments

I found some article how to return view to string in ASP.NET, but could not covert any to be able to run it with .NET Core

public static string RenderViewToString(this Controller controller, string viewName, object model)
{
    var context = controller.ControllerContext;
    if (string.IsNullOrEmpty(viewName))
        viewName = context.RouteData.GetRequiredString("action");

    var viewData = new ViewDataDictionary(model);

    using (var sw = new StringWriter())
    {
        var viewResult = ViewEngines.Engines.FindPartialView(context, viewName);
        var viewContext = new ViewContext(context, viewResult.View, viewData, new TempDataDictionary(), sw);
        viewResult.View.Render(viewContext, sw);

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

which assumed to be able to call from a Controller using:

var strView = this.RenderViewToString("YourViewName", yourModel);

When I try to run the above into .NET Core I get lots of compilation errors.

I tried to convert it to work with .NET Core, but failed, can anyone help with mentioning the required using .. and the required "dependencies": { "Microsoft.AspNetCore.Mvc": "1.1.0", ... }, to be used in the project.json.

some other sample codes are here and here and here

NOTE I need the solution to get the view converted to string in .NET Core, regardless same code got converted, or another way that can do it.

7 comments

@Pharylon 2018-12-05 19:24:20

Red's answer got me 99% of the way there, but it doesn't work if your views are in an unexpected location. Here's my fix for that.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System.IO;
using System.Threading.Tasks;

namespace Example
{
    public static class ControllerExtensions
    {
        public static async Task<string> RenderViewAsync<TModel>(this Controller controller, string viewName, TModel model, bool isPartial = false)
        {
            if (string.IsNullOrEmpty(viewName))
            {
                viewName = controller.ControllerContext.ActionDescriptor.ActionName;
            }

            controller.ViewData.Model = model;

            using (var writer = new StringWriter())
            {
                IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
                ViewEngineResult viewResult = GetViewEngineResult(controller, viewName, isPartial, viewEngine);

                if (viewResult.Success == false)
                {
                    throw new System.Exception($"A view with the name {viewName} could not be found");
                }

                ViewContext viewContext = new ViewContext(
                    controller.ControllerContext,
                    viewResult.View,
                    controller.ViewData,
                    controller.TempData,
                    writer,
                    new HtmlHelperOptions()
                );

                await viewResult.View.RenderAsync(viewContext);

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

        private static ViewEngineResult GetViewEngineResult(Controller controller, string viewName, bool isPartial, IViewEngine viewEngine)
        {
            if (viewName.StartsWith("~/"))
            {
                var hostingEnv = controller.HttpContext.RequestServices.GetService(typeof(IHostingEnvironment)) as IHostingEnvironment;
                return viewEngine.GetView(hostingEnv.WebRootPath, viewName, !isPartial);
            }
            else
            {
                return viewEngine.FindView(controller.ControllerContext, viewName, !isPartial);

            }
        }
    }
}

This allows you to use it as as below:

var emailBody = await this.RenderViewAsync("~/My/Different/View.cshtml", myModel);

@gtrak 2019-08-27 15:25:26

This answer has an issue, since it mutates the controllers own model controller.ViewData.Model = model;, we have to undo any changes or it breaks the followup view rendering. I wrapped this mutation and rendering in a try-finally to fix it.

@Chan 2018-11-08 04:59:55

I tried the solution which answered by @Hasan A Yousef in Dotnet Core 2.1, but the csthml do not work well to me. It always throws a NullReferenceException, see screenshot. enter image description here

To solve it, I assign the Html.ViewData.Model to a new object. Here is my code.

@page
@model InviteViewModel 
@{
    var inviteViewModel = Html.ViewData.Model;
}

<p>
    <strong>User Id:</strong> <code>@inviteViewModel.UserId </code>
</p>

@gbade_ 2018-11-27 11:33:53

I tried your method and now I'm getting this - Executed action Controllers.PortfolioController.PrintStatement in [ERR] An unhandled exception has occurred while executing the request System.NullReferenceException: Object reference not set to an instance of an object. at AspNetCore._Views_Portfolio_PrintStatement_cshtml. in PrintStatement.cshtml:line 248. --------------Prior to that, I was getting the nullreference error at line 0 in the cshtml file.

@Roberto 2018-12-07 21:58:17

This does work, but the issue is the @page directive that marks it as a Razor Page. And Razor Pages work differently than Razor Views. See this other SO solution for a way to add the Model to a Razor Page: stackoverflow.com/a/49275145/943435

@Red 2018-04-25 13:57:14

If like me you have a number of controllers that need this, like in a reporting site, it's not really ideal to repeat this code, and even injecting or calling another service doesn't really seem right.

So I've made my own version of the above with the following differences:

  • model strong-typing
  • error checking when finding a view
  • ability to render views as partials or pages
  • asynchronus
  • implemented as a controller extension
  • no DI needed

    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Microsoft.AspNetCore.Mvc.ViewEngines;
    using Microsoft.AspNetCore.Mvc.ViewFeatures;
    using System.IO;
    using System.Threading.Tasks;
    
    namespace CC.Web.Helpers
    {
        public static class ControllerExtensions
        {
            public static async Task<string> RenderViewAsync<TModel>(this Controller controller, string viewName, TModel model, bool partial = false)
            {
                if (string.IsNullOrEmpty(viewName))
                {
                    viewName = controller.ControllerContext.ActionDescriptor.ActionName;
                }
    
                controller.ViewData.Model = model;
    
                using (var writer = new StringWriter())
                {
                    IViewEngine viewEngine = controller.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine)) as ICompositeViewEngine;
                    ViewEngineResult viewResult = viewEngine.FindView(controller.ControllerContext, viewName, !partial);
    
                    if (viewResult.Success == false)
                    {
                        return $"A view with the name {viewName} could not be found";
                    }
    
                    ViewContext viewContext = new ViewContext(
                        controller.ControllerContext,
                        viewResult.View,
                        controller.ViewData,
                        controller.TempData,
                        writer,
                        new HtmlHelperOptions()
                    );
    
                    await viewResult.View.RenderAsync(viewContext);
    
                    return writer.GetStringBuilder().ToString();
                }
            }
        }
    }
    

Then just implement with:

viewHtml = await this.RenderViewAsync("Report", model);

Or this for a PartialView:

partialViewHtml = await this.RenderViewAsync("Report", model, true);

@DGaspar 2018-05-11 10:40:07

This one should be way more upthere. It looks way more elegant and it worked like a charm for me.

@LentoMan 2018-05-18 14:25:57

Thanks, excellent solution! The only thing I changed was adding an additional wrapped extension method for partial views. I actually ran into a bit of a problem when using the previously injected ViewRenderService since it could access partial views from the view tree of other controllers. Those views did render correctly, but would never automatically recompile during debug, moving them to Shared views solved the issue!

@Mike Moore 2018-06-02 22:18:44

@LentoMan could you post an example of the wrapper extension that you mentioned?

@LentoMan 2018-06-04 10:47:34

Just add another method in the same extension class that Red suggested: ` public static async Task<string> RenderPartialViewAsync<TModel>(this Controller controller, string viewName, TModel model) { return await controller.RenderViewAsync(viewName, model, true); }`

@Red 2018-06-04 14:08:23

If that code does what you need, you could just directly call controller.RenderViewAsync(viewName, model, true);. I don't see you're gaining anything, other than making it clearer that it's a Partial, but that could be important in your situation.

@LentoMan 2018-06-05 16:02:50

You are not wrong, one could also use named arguments to make it more readable. But in my case, I mainly use partials, so it is more about making it easier to use and matching the result methods already available on the controller.

@Daniel Gregatto 2018-06-29 14:37:49

Thanks, excellent solution!

@Janis Veinbergs 2018-08-20 08:05:51

I would prefer throwing exception if view could not be found rather than getting surprise result: return $"A view with the name {viewName} could not be found";

@Pharylon 2018-12-05 19:22:34

This doesn't work if your views are in an unusual location. I posted a fix for that in my own answer.

@jonmeyer 2018-12-24 15:09:26

@Red 3 changes i would recommend: 1. use GetRequiredService instead of GetService 2. Resolve generic iLogger<> to log when success is false to record the error message 3. Resolve IOptions<HtmlHelperOptions> instead of using new HtmlHelperOptions()

@Faraz Ahmed 2019-05-16 09:19:37

Previously I have coded with different technique it works in development but not in IIS, finally your code work in both dev and live.

@Adrita Sharma 2019-05-16 18:48:09

What should be passed in controller in (this Controller controller, string viewName, TModel model, bool partial = false). I am getting compile time error.

@Red 2019-05-21 16:08:21

@Adrita-Sharma - it's a controller extension, so you don't pass in your controller, it's a function added to all controllers. So whatever controller produces the view you want as a string, use the implementation code in the answer there.

@Watson 2019-08-29 23:24:26

How do you let javascript run to completion? Any view returned does not run js - for example: <div id="Test"></div> and document.getElementById("Test").innerHTML = "whatever". Returned content does not change inner div html.

@Red 2019-08-30 08:56:57

This is run on the server, so no client code will be executed. If you can't refactor to use the model data I think you're out of luck

@Hasan A Yousef 2016-12-02 13:07:36

Thanks to Paris Polyzos and his article.

I'm re-posting his code here, just in case the original post got removed for any reason.

Create Service in file viewToString.cs as below code:

using System;
using System.IO;
using System.Threading.Tasks;
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.ViewFeatures;
using Microsoft.AspNetCore.Routing;
 
namespace WebApplication.Services
{
    public interface IViewRenderService
    {
        Task<string> RenderToStringAsync(string viewName, object model);
    }
 
    public class ViewRenderService : IViewRenderService
    {
        private readonly IRazorViewEngine _razorViewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;
 
        public ViewRenderService(IRazorViewEngine razorViewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _razorViewEngine = razorViewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }
 
        public async Task<string> RenderToStringAsync(string viewName, object model)
        {
            var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
 
            using (var sw = new StringWriter())
            {
                var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
 
                if (viewResult.View == null)
                {
                    throw new ArgumentNullException($"{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();
            }
        }
    }
}
  1. Add the service to the Startup.cs file, as:

    using WebApplication.Services;
    
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        services.AddScoped<IViewRenderService, ViewRenderService>();
     }
    

Add "preserveCompilationContext": true to the buildOptions in the project.json, so the file looks like:

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable",
    "emitEntryPoint": true,
    "preserveCompilationContext": true
  },
  "dependencies": {
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Microsoft.AspNetCore.Mvc": "1.0.1"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.1"
        }
      },
      "imports": "dnxcore50"
    }
  }
}
  1. Define you model, for example:

    public class InviteViewModel {
        public string   UserId {get; set;}
        public string   UserName {get; set;}
        public string   ReferralCode {get; set;}
        public int  Credits {get; set;}
    }
    
  2. Create your Invite.cshtml for example:

    @{
        ViewData["Title"] = "Contact";
    }
    @ViewData["Title"].
    user id: @Model.UserId
    
  3. In the Controller:

a. Define the below at the beginning:

private readonly IViewRenderService _viewRenderService;
 
public RenderController(IViewRenderService viewRenderService)
{
    _viewRenderService = viewRenderService;
}

b. Call and return the view with model as below:

var result = await _viewRenderService.RenderToStringAsync("Email/Invite", viewModel);
return Content(result);

c. The FULL controller example, could be like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

using WebApplication.Services;

namespace WebApplication.Controllers
{
[Route("render")]
public class RenderController : Controller
{
    private readonly IViewRenderService _viewRenderService;
 
    public RenderController(IViewRenderService viewRenderService)
    {
        _viewRenderService = viewRenderService;
    }
 
    [Route("invite")]
    public async Task<IActionResult> RenderInviteView()
    {
        ViewData["Message"] = "Your application description page.";
        var viewModel = new InviteViewModel
        {
            UserId = "cdb86aea-e3d6-4fdd-9b7f-55e12b710f78",
            UserName = "Hasan",
            ReferralCode = "55e12b710f78",
            Credits = 10
        };
 
        var result = await _viewRenderService.RenderToStringAsync("Email/Invite", viewModel);
        return Content(result);
    }
}

public class InviteViewModel {
        public string   UserId {get; set;}
        public string   UserName {get; set;}
        public string   ReferralCode {get; set;}
        public int  Credits {get; set;}
} 
}

@philw 2017-11-01 17:50:28

That pretty much works for Core 2.0, with two exceptions: (1) _razorViewEngine.FindView doesn't work on absolute paths, and I at least need those because the standard template apps don't use Views folders which it assumes. Tto use his is documented as "by design" on the Core 2.0 GitHub site, and the solution is to use _razorViewEngine.GetView, which supports absolute paths. (2) that preserveCompilationContext (not in the original article) isn't explained - why do you need it? It's not clear where to put it with COre 2.0, and it seems to work without it.

@martonx 2018-02-25 22:56:36

With this code ViewData["Message"] = "Your application description page."; will be null in the view. Why? Could anybody post a fixed version which contains correct handling of ViewData, not just view model.

@Chan 2018-11-08 05:02:09

check my answer in the post. I pasted a sample code of cshtml.

@scgough 2018-12-14 10:12:37

This is nice. I'm having a small issue though. I want to get the HTML string for ControllerB.Action1 from ControllerA.Action1. The code seems to successfully render the view to a string but it doesn't seem to execute the ControllerB.Action1 method before hand. Is there a way to get this to happen? I am using a dynamic partial view, not a static shared partial view.

@c-sharp 2018-12-27 00:19:29

How do you write a unit test for this service? I mean, how do we get the dependencies?

@Felype 2019-02-01 16:05:00

This is great, though I had to change the interface from receiving an object model to Receiving the ViewDataDictionary ViewData instead, and then instead of initializing the var viewDictionary as you did, I simply use the ViewData. This way I get the Model, ViewBag, ViewData and everything else and that the controller context had, just as if I was calling View()

@haugan 2019-04-24 11:44:46

I get this error, trying this in .NET Core 2.2 from a Web API controller: "Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' while attempting to activate 'xxx.Api.ViewRenderService'." I think I've followed the instructions form Hasan's answer to the point. What could be wrong?

@Andrew 2019-08-25 20:50:13

This works great for me. I changed this so that it gets the ActionContext via the IActionContextAccessor interface.

@Dave Glassborow 2018-01-04 15:37:23

The answers above are fine, but need to tweaking to get any tag helpers to work (we need to use the actually http context). Also you will need to explicitly set the layout in the view to get a layout rendered.

public class ViewRenderService : IViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;
    private readonly IHostingEnvironment _env;
    private readonly HttpContext _http;

    public ViewRenderService(IRazorViewEngine razorViewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider, IHostingEnvironment env, IHttpContextAccessor ctx)
    {
        _razorViewEngine = razorViewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; _env = env; _http = ctx.HttpContext;
    }

    public async Task<string> RenderToStringAsync(string viewName, object model)
    {
        var actionContext = new ActionContext(_http, new RouteData(), new ActionDescriptor());

        using (var sw = new StringWriter())
        {
            var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
            //var viewResult = _razorViewEngine.GetView(_env.WebRootPath, viewName, false); // For views outside the usual Views folder
            if (viewResult.View == null)
            {
                throw new ArgumentNullException($"{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(_http, _tempDataProvider), sw, new HtmlHelperOptions());
            viewContext.RouteData = _http.GetRouteData();
            await viewResult.View.RenderAsync(viewContext);
            return sw.ToString();
        }
    }
}

@Dave Glassborow 2018-01-12 11:55:27

Note: on azure, I needed to add the following to the Startup services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); Weirdly worked fine locally without this ...

@Richard Mneyan 2017-06-28 14:17:25

The link below tackles pretty much the same issue:

Where are the ControllerContext and ViewEngines properties in MVC 6 Controller?

In Hasan A Yousef's answer I had to make the same change as in the link above to make it work me:

using Microsoft.AspNetCore.Hosting;
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.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.IO;
using System.Threading.Tasks;

public class ViewRenderService : IViewRenderService
{
    private readonly IRazorViewEngine _razorViewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;
    private readonly IHostingEnvironment _env;

    public ViewRenderService(IRazorViewEngine razorViewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider, IHostingEnvironment env)
    {
        _razorViewEngine = razorViewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; _env = env;
    }

    public async Task<string> RenderToStringAsync(string viewName, object model)
    {
        var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
        var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

        using (var sw = new StringWriter()) {
            //var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
            var viewResult = _razorViewEngine.GetView(_env.WebRootPath, viewName, false);
            if (viewResult.View == null) {
                throw new ArgumentNullException($"{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();
        }
    }

@Piotr Kula 2018-11-16 14:44:44

Thanks- I was looking for something like this to merge a model in HTML using Razor. Not sure why MS wont just make a simpler way of this doing. Although.. its much easier than in MVC 1. Great solution and thanks for sharing the GetView change!

@Richard Mneyan 2018-11-16 15:26:29

Your welcome @ppumkin, back then I spent long time figuring this out, and I needed this badly.

@John Davidson 2016-12-01 20:04:09

Microsoft has an excellent article on Controller Testing at https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing

Once you have returned a ViewResult then you can get the string content by

var strResult = ViewResult.Content

@Hasan A Yousef 2016-12-02 10:48:10

I got error CS0117: 'ViewResult' doesn't contain a definition for 'Content'

Related Questions

Sponsored Content

39 Answered Questions

24 Answered Questions

[SOLVED] JavaScriptSerializer - JSON serialization of enum as string

63 Answered Questions

[SOLVED] What is the difference between String and string in C#?

11 Answered Questions

[SOLVED] How do I import a namespace in Razor View Page?

10 Answered Questions

24 Answered Questions

[SOLVED] Case insensitive 'Contains(string)'

12 Answered Questions

[SOLVED] How to determine if .NET Core is installed

15 Answered Questions

[SOLVED] Easiest way to split a string on newlines in .NET?

  • 2009-10-10 09:25:56
  • RCIX
  • 480389 View
  • 735 Score
  • 15 Answer
  • Tags:   c# .net string split

9 Answered Questions

[SOLVED] How to escape braces (curly brackets) in a format string in .NET

1 Answered Questions

Sponsored Content