using Microsoft.AspNetCore.Html; 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.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using System.Diagnostics; using System.IO; using System.Text.Encodings.Web; namespace zero.Rendering; public class RazorRenderer : IRazorRenderer, IDisposable { protected IRazorViewEngine ViewEngine { get; set; } protected IRazorPageActivator PageActivator { get; set; } protected ITempDataDictionaryFactory TempDataDictionaryFactory { get; set; } protected IModelMetadataProvider ModelMetadataProvider { get; set; } protected IHttpContextAccessor HttpContextAccessor { get; set; } protected IServiceScope ServiceScope { get; set; } protected HtmlHelperOptions HtmlHelperOptions { get; set; } public RazorRenderer(IRazorViewEngine viewEngine, IRazorPageActivator pageActivator, IHttpContextAccessor httpContextAccessor, ITempDataDictionaryFactory tempDataDictionaryFactory, IModelMetadataProvider modelMetadataProvider, IServiceProvider serviceProvider, IOptions mvcHelperOptions) { ViewEngine = viewEngine; PageActivator = pageActivator; HttpContextAccessor = httpContextAccessor; TempDataDictionaryFactory = tempDataDictionaryFactory; ModelMetadataProvider = modelMetadataProvider; ServiceScope = serviceProvider.CreateScope(); HtmlHelperOptions = mvcHelperOptions.Value.HtmlHelperOptions; } /// /// Renders a razor component to a string /// public async Task ComponentAsync(object args = null) where T : ViewComponent { return await ComponentAsync(typeof(T), args); } /// /// Renders a razor component to a string /// public async Task ComponentAsync(Type componentType, object args = null) { return await ComponentAsync(componentType, BuildActionContext(), args); } /// /// Renders a razor component to a string /// public async Task ComponentAsync(string componentName, object args = null) { return await ComponentAsync(componentName, BuildActionContext(), args); } /// /// Renders a razor component to a string /// public async Task ComponentAsync(ActionContext context, object args = null) where T : ViewComponent { return await ComponentAsync(typeof(T), context, args); } /// /// Renders a razor component to a string /// public async Task ComponentAsync(Type componentType, ActionContext context, object args = null) { IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); using StringWriter stringWriter = new(); ViewContext viewContext = BuildViewContext(context, stringWriter); (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); IHtmlContent result = await viewComponentHelper.InvokeAsync(componentType, args); result.WriteTo(stringWriter, HtmlEncoder.Default); await stringWriter.FlushAsync(); return stringWriter.ToString(); } /// /// Renders a razor component to a string /// public async Task ComponentAsync(string componentName, ActionContext context, object args = null) { IViewComponentHelper viewComponentHelper = context.HttpContext.RequestServices.GetRequiredService(); using StringWriter stringWriter = new(); ViewContext viewContext = BuildViewContext(context, stringWriter); (viewComponentHelper as IViewContextAware)?.Contextualize(viewContext); IHtmlContent result = await viewComponentHelper.InvokeAsync(componentName, args); result.WriteTo(stringWriter, HtmlEncoder.Default); await stringWriter.FlushAsync(); return stringWriter.ToString(); } /// /// Renders a razor page to a string /// public async Task PageAsync(T model) where T : PageModel { IRazorPage pageResult = FindPage(model.PageContext, model.PageContext.ActionDescriptor.RelativePath); using StringWriter stringWriter = new(); RazorView view = new RazorView(ViewEngine, PageActivator, new List(), pageResult, HtmlEncoder.Default, new DiagnosticListener("ViewRenderService")); ViewContext viewContext = BuildViewContext(model.PageContext, stringWriter, view); viewContext.RouteData = model.RouteData; viewContext.ViewData = model.ViewData; Microsoft.AspNetCore.Mvc.RazorPages.Page razorPage = (Microsoft.AspNetCore.Mvc.RazorPages.Page)pageResult; razorPage.PageContext = model.PageContext; razorPage.ViewContext = viewContext; PageActivator.Activate(razorPage, viewContext); await razorPage.ExecuteAsync(); await stringWriter.FlushAsync(); return stringWriter.ToString(); } /// /// Renders a razor view to a string /// public async Task ViewAsync(string view, object model = null) { return await ViewAsync(BuildActionContext(), view, model); } /// /// Renders a razor view to a string /// public async Task ViewAsync(ActionContext context, string view, object model = null) { IView viewResult = FindView(context, view); using StringWriter stringWriter = new(); ViewContext viewContext = BuildViewContext(context, stringWriter, viewResult); viewContext.RouteData = context.RouteData; viewContext.ViewData.Model = model; await viewResult.RenderAsync(viewContext); await stringWriter.FlushAsync(); return stringWriter.ToString(); } /// public void Dispose() { ServiceScope?.Dispose(); } /// /// Build the view context /// protected virtual ViewContext BuildViewContext(ActionContext context, StringWriter writer, IView view = null) { ViewDataDictionary viewData = new(ModelMetadataProvider, context.ModelState); // result.ViewData ?? new ViewData...; ITempDataDictionary tempData = TempDataDictionaryFactory.GetTempData(context.HttpContext); // result.TempData ?? TempData...; return new ViewContext(context, view ?? NullView.Instance, viewData, tempData, writer, HtmlHelperOptions); } /// /// Builds a new view context /// protected virtual ActionContext BuildActionContext() { HttpContext context = GetHttpContext(); RouteData routeData = context.GetRouteData(); return new ActionContext(context, routeData, new ActionDescriptor()); } /// /// Get HTTP context or mock one /// protected virtual HttpContext GetHttpContext() { HttpContext context = HttpContextAccessor.HttpContext; context ??= new DefaultHttpContext() { RequestServices = ServiceScope.ServiceProvider }; return context; } /// /// Tries to find a view /// protected virtual IView FindView(ActionContext actionContext, string viewName) { ViewEngineResult getViewResult = ViewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false); if (getViewResult.Success) { return getViewResult.View; } ViewEngineResult findViewResult = ViewEngine.FindView(actionContext, viewName, isMainPage: false); if (findViewResult.Success) { return findViewResult.View; } IEnumerable searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations); string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations)); throw new InvalidOperationException(errorMessage); } /// /// Tries to find a page /// protected virtual IRazorPage FindPage(ActionContext actionContext, string pageName) { RazorPageResult getPageResult = ViewEngine.GetPage(executingFilePath: null, pagePath: pageName); if (getPageResult.Page != null) { return getPageResult.Page; } RazorPageResult findPageResult = ViewEngine.FindPage(actionContext, pageName: pageName); if (findPageResult.Page != null) { return findPageResult.Page; } IEnumerable searchedLocations = getPageResult.SearchedLocations.Concat(findPageResult.SearchedLocations); string errorMessage = String.Join(Environment.NewLine, new[] { $"Unable to find page '{pageName}'. The following locations were searched:" }.Concat(searchedLocations)); throw new InvalidOperationException(errorMessage); } class GenericController : ControllerBase { } 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; } } } public interface IRazorRenderer { /// /// Renders a razor component to a string /// Task ComponentAsync(object args = null) where T : ViewComponent; /// /// Renders a razor component to a string /// Task ComponentAsync(Type componentType, object args = null); /// /// Renders a razor component to a string /// Task ComponentAsync(string componentName, object args = null); /// /// Renders a razor component to a string /// Task ComponentAsync(ActionContext context, object args = null) where T : ViewComponent; /// /// Renders a razor component to a string /// Task ComponentAsync(Type componentType, ActionContext context, object args = null); /// /// Renders a razor component to a string /// Task ComponentAsync(string componentName, ActionContext context, object args = null); /// /// Renders a razor page to a string /// Task PageAsync(T model) where T : PageModel; /// /// Renders a razor view to a string /// Task ViewAsync(string view, object model = null); /// /// Renders a razor view to a string /// Task ViewAsync(ActionContext context, string view, object model = null); }