5

I have the following code that is executed by Hangfire (there is no HttpContext) and works perfectly when I run it locally:

class FakeController : ControllerBase
{
    protected override void ExecuteCore() { }

    public static string RenderViewToString(string controllerName, string viewName, object viewData)
    {
        using (var writer = new StringWriter())
        {
            var routeData = new RouteData();
            routeData.Values.Add("controller", controllerName);
            var fakeControllerContext = new ControllerContext(
                                                              new HttpContextWrapper(
                                                                                     new HttpContext(new HttpRequest(null, "http://nopage.no", null)
                                                                                                   , new HttpResponse(null))
                                                                                     ), 
                                                              routeData, 
                                                              new FakeController()
                                                            );
            var razorViewEngine = new RazorViewEngine();
            var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);

            var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
            razorViewResult.View.Render(viewContext, writer);
            return writer.ToString();

        }
    }
}

The way we have our application set up however is as follows:

https://application.net <- One application

https://application.net/admin <- The other application that this code runs on.

When I run the code on https://application.net/admin, I get the following exception:

System.ArgumentException: The virtual path '/' maps to another application, which is not allowed.

It occurs at this line: razorViewEngine.FindView(fakeControllerContext, viewName, "", false).

I tried creating my own RazorViewEngine and overrode some of the methods to find the views, to no avail.

class MyViewEngine : RazorViewEngine
{
    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        if (!base.FileExists(controllerContext, virtualPath))
            return base.FileExists(controllerContext, "~/../admin" + virtualPath.TrimStart('~'));

        return true;
    }
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        var newViewPath = viewPath;
        if (!base.FileExists(controllerContext, viewPath))
            newViewPath = "~/../admin/" + viewPath.TrimStart('~');

        return base.CreateView(controllerContext, newViewPath, masterPath);
    }
}

This failed because it did not want to let me out of the "upmost part of the directory tree".

Is there an easy fix for this, or an alternate way of creating strings from razor views? - The purpose is to create email templates. There will be many emails created, and I don't want to use a HttpClient to ask my own endpoint to create it.

Bjørn
  • 1,138
  • 2
  • 16
  • 47
  • Does this answer your question? [ASP.NET Virtual Path Maps To Another Application Which Is Not Allowed](https://stackoverflow.com/questions/19277350/asp-net-virtual-path-maps-to-another-application-which-is-not-allowed) – Hintham Jun 26 '20 at 12:18
  • Thanks, but not really. It works just fine locally. No problem there. It's only when I host it on a sub-url in an existing application. I think it might call the controller on https://application.net instead of the controller on https://application.net/admin – Bjørn Jun 26 '20 at 12:23

2 Answers2

0

I was able to replicated issue. httpContext was not getting set correctly. Change code this way and no need to override RazorViewEngine:

static string GetUrlRoot()
 {
    var httpContext = HttpContext.Current;
    if (httpContext == null)
     {
        return "http://localhost";
     }

    return httpContext.Request.Url.GetLeftPart(UriPartial.Authority) +
               httpContext.Request.ApplicationPath;
}

public static string RenderViewToString(string controllerName, string viewName, object viewData)
 {
    using (var writer = new StringWriter())
        {
            var routeData = new RouteData();
            routeData.Values.Add("controller", controllerName);
            var fakeControllerContext = new ControllerContext(
                                                              new HttpContextWrapper(
                                                                                     new HttpContext(new HttpRequest(null, GetUrlRoot(), null)
                                                                                                   , new HttpResponse(null))
                                                                                     ),
                                                              routeData,
                                                              new FakeController()
                                                            );
            var razorViewEngine = new RazorViewEngine();
            var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);

            var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer);
            razorViewResult.View.Render(viewContext, writer);
            return writer.ToString();

        }
}
Always_a_learner
  • 1,254
  • 1
  • 8
  • 16
  • For me, this would change "http://nopage.no" to "http://localhost" since HttpContext.Current is null. The code is run in a Hangfire job (without a HttpContext), and I would suppose that it would not work then? - Which is the reason why I am creating the HttpRequest, Response and putting them in the HttpContext in the first place. Following your example, i could just as well have put HttpContext.Current as an argument into HttpContextWrapper and be done with it. – Bjørn Jun 29 '20 at 09:25
  • about hangfire you might be right. however i would be happy if it help you in any way – Always_a_learner Jun 29 '20 at 09:43
  • I would be happy too :) But I am afraid this is not helping much. Thanks for trying :) – Bjørn Jun 29 '20 at 09:48
  • I'm unsure what you are asking for. I added the fact that there is no HttpContext and that the job is executed by Hangfire in the question. Other than that I believe the question is descriptive enough? If you have a concrete question i'd be more than happy to clearify. – Bjørn Jun 29 '20 at 09:51
  • I would suggest add you was trying in Hangfire..it will give more relevant information to post – Always_a_learner Jun 29 '20 at 09:51
  • Yes. I just did that :) – Bjørn Jun 29 '20 at 09:52
  • i just read you can build httpcontext in hangfire..refer https://stackoverflow.com/questions/47404479/add-httpcontext-into-hangfire – Always_a_learner Jun 29 '20 at 10:03
  • https://discuss.hangfire.io/t/alternative-for-missing-httpcontent/107 – Always_a_learner Jun 29 '20 at 10:04
  • You might be on to something here, i'm sorry, but i have not had time to look closer at it because of other issues. I will get back to it as soon as possible. – Bjørn Jul 02 '20 at 14:15
  • This did not help unfortunatley, but instead of wasting the points, I'll give them to you. I'll also post my own answer of how I ended up solving this. Thank's for trying to help. I really appreciate it. – Bjørn Jul 06 '20 at 13:46
0

After a lot of trying and failing I ended up abandoning my approach, using Rick Strahl's WestWind.RazorHosting library instead.

Here's the most important part of the documentation I used.

And here is how my code ended up looking at the end.

class FakeController : IDisposable
{
    private readonly RazorFolderHostContainer _host;
    public FakeController()
    {
        _host = new RazorFolderHostContainer
               {
                   TemplatePath = $@"{HostingEnvironment.ApplicationPhysicalPath}Views\EmailTemplate\"
               };

        _host.AddAssemblyFromType(typeof(Controller));
        _host.AddAssemblyFromType(typeof(EmailTemplateController.TestViewModel));
        _host.Start();
    }

    public string RenderViewToString(string viewName, object viewData)
    {
        return _host.RenderTemplate($@"~/{viewName}.cshtml", viewData);
    }

    public void Dispose()
    {
        _host.Stop();
    }
}

This looks for views in my EmailTemplate folder that's placed in Views, finding *.cshtml files that I want to use. Since it gets them from the Views folder, I'm still able to use the same views with the normal controllers, and hence it will work when I use the views in my endpoints too. Nifty.

Bjørn
  • 1,138
  • 2
  • 16
  • 47