soundaranbu/Razor.Templating.Core

Add ability to provide an optional Controller Name

dammitjanet opened this issue · 5 comments

One of the issues I've come across is that all your View references need to absolute and the renderer cannot find views within the controller specific Views directory.

Even if you specify the absolute path, if you have views that call other views, such as EditorTemplates within the controller specific Views directory, then those views cannot be found

So the ability to add an optional controller name looks like this.

			RouteData routeData = new();
			if (!string.IsNullOrWhiteSpace(controllerName))
			{
				routeData.Values.Add("controller", controllerName!);
			}

passing that routeData into the ActionContext constructor allows the Views relative to the "Views/{controllerName}/ path to be found, so something like this would be the fix, obviously the need then becomes such that you then need to pass in the controllerName to those methods that call it

        private ActionContext GetActionContext(string? controllerName = null)
        {
            var httpContext = new DefaultHttpContext
            {
                RequestServices = _serviceProvider
            };
            var app = new ApplicationBuilder(_serviceProvider);
            var routeBuilder = new RouteBuilder(app)
            {
                DefaultHandler = new CustomRouter()
            };

            routeBuilder.MapRoute(
                string.Empty,
                "{controller}/{action}/{id}",
                new RouteValueDictionary(new { id = "defaultid" }));

			RouteData routeData = new();
			if (!string.IsNullOrWhiteSpace(controllerName))
			{
				routeData.Values.Add("controller", controllerName!);
			}

            var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
            actionContext.RouteData.Routers.Add(routeBuilder.Build());
            return actionContext;
        }

Hey @dammitjanet, thanks for raising the issue.

This needs some investigation to see if it's possible to dynamically get the currently executing controller name in the MVC project.

For other types of workloads like Console, Worker Service, I believe this shouldn't apply.

For other types of workloads like Console, Worker Service, I believe this shouldn't apply.

yeah, agreed

Here's some code (based on an SO answer here, improved over time and has the ability as noted above. Hopefully it might prove useful as it works as expected.

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 My.AspNetCore.MVC
{
	public class HtmlRenderService(IServiceProvider serviceProvider) : IHtmlRenderService
	{
		#region IHtmlRenderService impls

		public string RenderToString(string view, object? model, string? controllerName = null)
		{
			using (var output = new StringWriter())
			{
				var (viewContext, viewEngineResult) = BuildRequest(view, model, controllerName, output);
				viewEngineResult.View!.RenderAsync(viewContext).GetAwaiter().GetResult();
				return output.ToString();
			}
		}

		public async Task<string> RenderToStringAsync(string view, object model, string? controllerName = null)
		{
			using (var output = new StringWriter())
			{
				var (viewContext, viewEngineResult) = BuildRequest(view, model, controllerName, output);
				await viewEngineResult.View!.RenderAsync(viewContext);
				return output.ToString();
			}
		}

		#endregion

		#region private methods

		private (ViewContext viewContext, ViewEngineResult viewEngineResult) BuildRequest(string view, object? model, string? controllerName, StringWriter output)
		{
			var httpContext = new DefaultHttpContext { RequestServices = serviceProvider };

			RouteData routeData = new();
			if (!string.IsNullOrWhiteSpace(controllerName))
			{
				routeData.Values.Add("controller", controllerName!);
			}

			var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());

			if (serviceProvider.GetService(typeof(ITempDataProvider)) is not ITempDataProvider tempDataProvider)
			{
				throw new Exception("Can't find ITempDataProvider");
			}

			if (serviceProvider.GetService(typeof(IRazorViewEngine)) is not IRazorViewEngine engine)
			{
				throw new Exception("Can't find IRazorViewEngine");
			}

			var viewEngineResult = engine.FindView(actionContext, view, false);

			if (viewEngineResult == null || !viewEngineResult.Success || viewEngineResult.View == null)
			{
				throw new InvalidOperationException($"Unable to find view '{view}'");
			}

			var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
			{
				Model = model
			};

			var viewContext = new ViewContext(actionContext, viewEngineResult.View!,
				viewDictionary, new TempDataDictionary(actionContext.HttpContext, tempDataProvider),
				output, new HtmlHelperOptions());

			return (viewContext, viewEngineResult);
		}
		
		#endregion
	}
}

That's nice. You are free to raise a PR if you're interested. Otherwise I can have a look at this when I get some free time.

Thanks!

Hey @dammitjanet, I'm pleased to share that the prerelease package has been published with the fix if you want to give it a try

https://github.com/soundaranbu/Razor.Templating.Core/releases/tag/v2.1.0-rc.1

You can also find the usage in the sample application here

This is now released in v2.1.0. Thanks for raising this issue :)