Problem:
I need to render a Razor Page partial to a string.
Why I want this:
I want to create a controller action that responds with JSON containing a partial view and other optional parameters.
Attempts:
I am familiar with the following example that renders a View to a string: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs
However, it is not compatible with Pages, as it only searches in the Views directory, so even if I give it an absolute path to the partial it tries to locate my _Layout.cshtml (which it shouldn't even do!) and fails to find it.
I have tried to modify it so it renders pages, but I end up getting a NullReferenceException for ViewData in my partial when attempting to render it. I suspect it has to do with NullView, but I have no idea what to put there instead (the constructor for RazorView requires many objects that I don't know how to get correctly).
The code:
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 // Modified by OronDF343: Uses pages instead of views. using System; using System.IO; using System.Linq; 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.Mvc.ViewFeatures.Internal; using Microsoft.AspNetCore.Routing; namespace TestAspNetCore.Services { public class RazorPageToStringRenderer { private readonly IRazorViewEngine _viewEngine; private readonly ITempDataProvider _tempDataProvider; private readonly IServiceProvider _serviceProvider; public RazorPageToStringRenderer( IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider) { _viewEngine = viewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; } public async Task<string> RenderPageToStringAsync<TModel>(string viewName, TModel model) { var actionContext = GetActionContext(); var page = FindPage(actionContext, viewName); using (var output = new StringWriter()) { var viewContext = new ViewContext(actionContext, new NullView(), new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model }, new TempDataDictionary(actionContext.HttpContext, _tempDataProvider), output, new HtmlHelperOptions()); page.ViewContext = viewContext; await page.ExecuteAsync(); return output.ToString(); } } private IRazorPage FindPage(ActionContext actionContext, string pageName) { var getPageResult = _viewEngine.GetPage(executingFilePath: null, pagePath: pageName); if (getPageResult.Page != null) { return getPageResult.Page; } var findPageResult = _viewEngine.FindPage(actionContext, pageName); if (findPageResult.Page != null) { return findPageResult.Page; } var searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations); var errorMessage = string.Join( Environment.NewLine, new[] { $"Unable to find page '{pageName}'. The following locations were searched:" }.Concat(searchedLocations)); throw new InvalidOperationException(errorMessage); } private ActionContext GetActionContext() { var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider }; return new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); } } }