I'm trying to render a Razor view to a string from a Hosted Service. By using the IRazorViewEngine
I am able to render a view to a string using something like the following:
_viewEngine.FindView(actionContext, viewName, false);
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()
);
viewContext.RouteData = httpContext.GetRouteData(); //set route data here
await viewResult.View.RenderAsync(viewContext);
However this falls apart when it is not called from a Controller
due to missing HttpContext
. I've tried building an HttpContext manually, but I get many errors and null exceptions deep in the Microsoft Mvc code which is extremely hard to debug. I've tried libraries like RazorLight which don't suit my needs because it doesn't properly support the @inject
directive. I think my best solution is to try and mock up a fake HttpContext/ControllerContext to pass to the native ViewEngine. However, when I create a new DefaultHttpContext
, I get a NullReferenceException around here, but it's very hard to trace the code and find where it is coming from.
Is there any way to create a new HttpContext?
You can mock it by creating a DefaultHttpContext
, however MVC requires some scoped services not present in the root DI scope, so you have to create a ServiceProvider
scope for your rendering.
Here is a sample IHostedService
that renders a view (I did run it in the WebApplication template with MVC):
public class ViewRenderService : IHostedService
{
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)
{
using (var requestServices = _serviceProvider.CreateScope())
{
var httpContext = new DefaultHttpContext { RequestServices = requestServices.ServiceProvider };
var routeData = new RouteData();
routeData.Values.Add("controller", "Home");
var actionContext = new ActionContext(httpContext, 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();
}
}
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var html = await RenderToStringAsync("About", null);
return;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
}
}
Note: This sample is based on a blog post found here, but modified to work in IHostedService
.
https://ppolyzos.com/2016/09/09/asp-net-core-render-view-to-string/
Try this:
public class YourClass
{
private readonly IHttpContextAccessor _httpContextAccessor;
public YourClass(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void YourMethod()
{
// access HttpContext with __httpContextAccessor.HttpContext
}
}
And then register IHttpContextAccessor
in the Startup class as follows:
public void ConfigureServices(IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Or you can also register as follows
services.AddHttpContextAccessor();
}
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