IsDevelopment().I'm using ASP.NET Core 3.1
I figured I'd try the new Razor Pages as they're advertised as super easy.
@page
@using MyProject.Pages.Pdf
@model IndexModel
<h2>Test</h2>
<p>
    @Model.Message
</p>
namespace MyProject.Pages.Pdf
{
    public class IndexModel : PageModel
    {
        private readonly MyDbContext _context;
        public IndexModel(MyDbContext context)
        {
            _context = context;
        }
        public string Message { get; private set; } = "PageModel in C#";
        public async Task<IActionResult> OnGetAsync()
        {
            var count = await _context.Foos.CountAsync();
            Message += $" Server time is { DateTime.Now } and the Foo count is { count }";
            return Page();
        }
    }
}
This works in the browser - yay!
I found Render a Razor Page to string which appears to do what I want.
But this is where the trouble start :(
First off, I find it very odd that when you find the page via _razorViewEngine.FindPage it doesn't know how to populate the ViewContext or the Model. I'd think the job of IndexModel was to populate these. I was hoping it was possible to ask ASP.NET for the IndexModel Page and that would be that.
Anyway... the next problem. In order to render the Page I have to manually create a ViewContext and I have to supply it with a Model. But the Page is the Model, and since it's a Page it isn't a simple ViewModel. It rely on DI and it expects OnGetAsync() to be executed in order to populate the Model. It's pretty much a catch-22.
I also tried fetching the View instead of the Page via _razorViewEngine.FindView but that also requires a Model, so we're back to catch-22.
Another issue. The purpose of the debug/tweaking page was to easily see what was generated. But if I have to create a Model outside IndexModel then it's no longer representative of what is actually being generated in a service somewhere.
All this have me wondering if I'm even on the right path. Or am I missing something?
Please refer to the following steps to render A Partial View To A String:
Add an interface to the Services folder named IRazorPartialToStringRenderer.cs.
 public interface IRazorPartialToStringRenderer
 {
     Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model);
 }
Add a C# class file to the Services folder named RazorPartialToStringRenderer.cs with the following code:
 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.ViewEngines;
 using Microsoft.AspNetCore.Mvc.ViewFeatures;
 using Microsoft.AspNetCore.Routing;
 namespace RazorPageSample.Services
 {
     public class RazorPartialToStringRenderer : IRazorPartialToStringRenderer
     {
         private IRazorViewEngine _viewEngine;
         private ITempDataProvider _tempDataProvider;
         private IServiceProvider _serviceProvider;
         public RazorPartialToStringRenderer(
             IRazorViewEngine viewEngine,
             ITempDataProvider tempDataProvider,
             IServiceProvider serviceProvider)
         {
             _viewEngine = viewEngine;
             _tempDataProvider = tempDataProvider;
             _serviceProvider = serviceProvider;
         }
         public async Task<string> RenderPartialToStringAsync<TModel>(string partialName, TModel model)
         {
             var actionContext = GetActionContext();
             var partial = FindView(actionContext, partialName);
             using (var output = new StringWriter())
             {
                 var viewContext = new ViewContext(
                     actionContext,
                     partial,
                     new ViewDataDictionary<TModel>(
                         metadataProvider: new EmptyModelMetadataProvider(),
                         modelState: new ModelStateDictionary())
                     {
                         Model = model
                     },
                     new TempDataDictionary(
                         actionContext.HttpContext,
                         _tempDataProvider),
                     output,
                     new HtmlHelperOptions()
                 );
                 await partial.RenderAsync(viewContext);
                 return output.ToString();
             }
         }
         private IView FindView(ActionContext actionContext, string partialName)
         {
             var getPartialResult = _viewEngine.GetView(null, partialName, false);
             if (getPartialResult.Success)
             {
                 return getPartialResult.View;
             }
             var findPartialResult = _viewEngine.FindView(actionContext, partialName, false);
             if (findPartialResult.Success)
             {
                 return findPartialResult.View;
             }
             var searchedLocations = getPartialResult.SearchedLocations.Concat(findPartialResult.SearchedLocations);
             var errorMessage = string.Join(
                 Environment.NewLine,
                 new[] { $"Unable to find partial '{partialName}'. 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());
         }
     }
 }
Register the services in the ConfigureServices method in the Startup class:
 public void ConfigureServices(IServiceCollection services)
 {
     services.AddRazorPages(); 
     services.AddTransient<IRazorPartialToStringRenderer, RazorPartialToStringRenderer>();
 }
Using the RenderPartialToStringAsync() method to render Razor Page as HTML string:
 public class ContactModel : PageModel
 {
     private readonly IRazorPartialToStringRenderer _renderer;
     public ContactModel(IRazorPartialToStringRenderer renderer)
     {
         _renderer = renderer; 
     }
     public void OnGet()
     { 
     }
     [BindProperty]
     public ContactForm ContactForm { get; set; }
     [TempData]
     public string PostResult { get; set; }
     public async Task<IActionResult> OnPostAsync()
     {
         var body = await _renderer.RenderPartialToStringAsync("_ContactEmailPartial", ContactForm);  //transfer model to the partial view, and then render the Partial view to string.
         PostResult = "Check your specified pickup directory";
         return RedirectToPage();
     }
 }
 public class ContactForm
 {
     public string Email { get; set; }
     public string Message { get; set; }
     public string Name { get; set; }
     public string Subject { get; set; }
     public Priority Priority { get; set; }
 }
 public enum Priority
 {
     Low, Medium, High
 }
The debug screenshot as below:

More detail steps, please check this blog Rendering A Partial View To A String.
I managed to crack it! I was on the wrong path after all... the solution is to use a ViewComponent. But it's still funky!
Thanks to
namespace MyProject.ViewComponents
{
    public class MyViewComponent : ViewComponent
    {
        private readonly MyDbContext _context;
        public MyViewComponent(MyDbContext context)
        {
            _context = context;
        }
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var count = await _context.Foos.CountAsync();
            var message = $"Server time is { DateTime.Now } and the Foo count is { count }";
            return View<string>(message);
        }
    }
}
and the view is placed in Pages/Shared/Components/My/Default.cshtml
@model string
<h2>Test</h2>
<p>
    @Model
</p>
using System;
using System.IO;
using System.Text.Encodings.Web;
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.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
public class RenderViewComponentService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IViewComponentHelper _viewComponentHelper;
    public RenderViewComponentService(
        IServiceProvider serviceProvider,
        ITempDataProvider tempDataProvider,
        IViewComponentHelper viewComponentHelper
    )
    {
        _serviceProvider = serviceProvider;
        _tempDataProvider = tempDataProvider;
        _viewComponentHelper = viewComponentHelper;
    }
    public async Task<string> RenderViewComponentToStringAsync<TViewComponent>(object args)
        where TViewComponent : ViewComponent
    {
        var viewContext = GetFakeViewContext();
        (_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
        var htmlContent = await _viewComponentHelper.InvokeAsync<TViewComponent>(args);
        using var stringWriter = new StringWriter();
        htmlContent.WriteTo(stringWriter, HtmlEncoder.Default);
        var html = stringWriter.ToString();
        return html;
    }
    private ViewContext GetFakeViewContext(ActionContext actionContext = null, TextWriter writer = null)
    {
        actionContext ??= GetFakeActionContext();
        var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
        var tempData = new TempDataDictionary(actionContext.HttpContext, _tempDataProvider);
        var viewContext = new ViewContext(
            actionContext,
            NullView.Instance,
            viewData,
            tempData,
            writer ?? TextWriter.Null,
            new HtmlHelperOptions());
        return viewContext;
    }
    private ActionContext GetFakeActionContext()
    {
        var httpContext = new DefaultHttpContext
        {
            RequestServices = _serviceProvider,
        };
        var routeData = new RouteData();
        var actionDescriptor = new ActionDescriptor();
        return new ActionContext(httpContext, routeData, actionDescriptor);
    }
    private class NullView : IView
    {
        public static readonly NullView Instance = new NullView();
        public string Path => string.Empty;
        public Task RenderAsync(ViewContext context)
        {
            if (context == null) { throw new ArgumentNullException(nameof(context)); }
            return Task.CompletedTask;
        }
    }
}
Note there is no code behind file
@page
@using MyProject.ViewComponents
@await Component.InvokeAsync(typeof(MyViewComponent))
@page "{id}"
@using MyProject.ViewComponents
@await Component.InvokeAsync(typeof(MyViewComponent), RouteData.Values["id"])
[HttpGet]
public async Task<IActionResult> Get()
{
    var html = await _renderViewComponentService
        .RenderViewComponentToStringAsync<MyViewComponent>();
    // do something with the html
    return Ok(new { html });
}
[HttpGet("{id}")]
public async Task<IActionResult> Get([FromRoute] int id)
{
    var html = await _renderViewComponentService
        .RenderViewComponentToStringAsync<MyViewComponent>(id);
    // do something with the html
    return Ok(new { html });
}
It's very unfortunate that the injected IViewComponentHelper doesn't work out of the box.
So we have do this very unintuitive thing to make it work.
(_viewComponentHelper as IViewContextAware).Contextualize(viewContext);
which causes a cascade of odd things like the fake ActionContext and ViewContext which require a TextWriter but it isn't used for ANYTHING! In fact the hole ViewContext isn't used at all. It just needs to exist :(
Also the NullView... for some reason Microsoft.AspNetCore.Mvc.ViewFeatures.NullView is Internal so we basically have to copy/paste it into our own code.
Perhaps it'll be improved in the future.
Anyway: IMO this is simpler then using IRazorViewEngine which turns up in pretty much every web search :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With