IRazorViewEngine.FindView with GetView can't find view

Viewed 2185

In my asp.net core project I'm trying to find Razor view using this method:

private IView FindView(ActionContext actionContext, string viewName)
{
    var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);
    if (getViewResult.Success)
    {
        return getViewResult.View;
    }

    var findViewResult = _viewEngine.FindView(actionContext, viewName, isMainPage: true);
    if (findViewResult.Success)
    {
        return findViewResult.View;
    }

    var searchedLocations = getViewResult.SearchedLocations.Concat(findViewResult.SearchedLocations);
    var errorMessage = string.Join(
        Environment.NewLine,
        new[] { $"Unable to find view '{viewName}'. The following locations were searched:" }.Concat(searchedLocations));

    throw new InvalidOperationException(errorMessage);
}

where

viewName = "Views/Email/ResetPassword.cshtml"

and _viewEngine is IRazorViewEngine, but it doesn't find any.

My project structure:

Project structure

IView.FindView method is called from Business.

I also have another project, that have the project structure and uses the same method for retrieving views and, more importantly, it finds this view, but it uses netcoreapp2.2, and my current project uses netcoreapp3.1 (Microsoft.AspNetCore.Mvc.Razor versions are the same - 2.2.0).

Why can't this method find views on .net core 3.1?


UPDATE

Both projects copy this Views folder to Api\bin\Debug\netcoreapp{version} folder on build.

2 Answers

Though I was building things from scratch in Core 3.1 and not upgrading from an earlier version, I ran into the same issue. I got things working by the doing the following:

I created an implementation of IWebHostEnvironment (I called mine DummyWebHostEnvironment.cs). I left all but one of the interface's properties with the default implementation; for that one property, I used the name of the project containing the views. (I just hardcoded it into the sample below for brevity; there are obviously slicker ways to obtain it.)

 public class DummyWebHostEnvironment : IWebHostEnvironment
    {
        public IFileProvider WebRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
        public string WebRootPath { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
        public string ApplicationName { get => "TheProjectContainingMyViews.RazorClassLibrary"; set => throw new System.NotImplementedException(); }
        public IFileProvider ContentRootFileProvider { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
        public string ContentRootPath { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
        public string EnvironmentName { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
}

Note: As is evident from the above code, the project containing the Views is a RazorClassLibrary. (I was using this and this as guidesfor getting the RazorViewEngine to work in a console application.)

One I had the implementation above, I added it to my services collection along with some other goodies:

private static RazorViewToStringRenderer GetRenderer()
        {
            var services = new ServiceCollection();
            var applicationEnvironment = PlatformServices.Default.Application;
            services.AddSingleton(applicationEnvironment);

            var appDirectory = Directory.GetCurrentDirectory();
    
            var environment = new DummyWebHostEnvironment();
            services.AddSingleton<IWebHostEnvironment>(environment);

            services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

            var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
            services.AddSingleton<DiagnosticSource>(diagnosticSource);
            services.AddSingleton<DiagnosticListener>(diagnosticSource);

            services.AddLogging();
            services.AddMvc();
            services.AddSingleton<RazorViewToStringRenderer>();
            var provider = services.BuildServiceProvider();
            return provider.GetRequiredService<RazorViewToStringRenderer>();
        }

Note: See the first of the links above for the code for RazorViewToStringRenderer. Here's the interface:

public interface IRazorViewToStringRenderer
    {
        Task<string> RenderViewToStringAsync<TModel>(string viewName, TModel model);
    }

Then, in Program.cs, I can just do something like this:

static async Task Main(string[] args)
        {
            var dto = BuildDto();
            var renderer = GetRenderer();

            var renderedString = await renderer.RenderViewToStringAsync("Views/Path/To/Some.cshtml", dto);
// ...

        }

I had the same issue. Although I am writing in .NET Core 5 already. I am assuming you are writing based on this or similar solution: https://scottsauber.com/2018/07/07/walkthrough-creating-an-html-email-template-with-razor-and-razor-class-libraries-and-rendering-it-from-a-net-standard-class-library/

You have

var getViewResult = _viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: true);

where executingFilePath is null.

Add executingFilePath so it leads to the view location on disk.

In my solution I have:

var getViewResult = _viewEngine.GetView(executingFilePath: executingFilePath, viewPath: viewName, isMainPage: true);

where executingFilePath is passed to RenderViewToStringAsync as additional parameter:

public class MessageBodyBuilderService : IMessageBodyBuilderService
{
    private readonly IRazorViewToStringRenderer _razorViewToStringRenderer;
    private readonly IWebHostEnvironment _hostingEnv;
    private readonly string _pathToEmailTemplates = $"/Views/EmailTemplates/";

    public MessageBodyBuilderService(
        IWebHostEnvironment hostingEnv, 
        IRazorViewToStringRenderer razorViewToStringRenderer)
    {
        _hostingEnv = hostingEnv;
        _razorViewToStringRenderer = razorViewToStringRenderer;
    }

    public async Task<BodyBuilder> BuildMessage<T>(string templateName, T modelForReplacement, bool isHtml = true)
    {
        string viewName = $"{_pathToEmailTemplates}{templateName}.cshtml";
        string body = await _razorViewToStringRenderer.RenderViewToStringAsync(viewName, modelForReplacement, _hostingEnv.ContentRootPath);

        var builder = new BodyBuilder()
        {
            HtmlBody = body
        };

        return builder;
    }
}

where _hostingEnv.ContentRootPath comes from the ContentRootPath I declared on Startup:

AppDomain.CurrentDomain.SetData("ContentRootPath", webHostEnvironment.ContentRootPath);

and then you can pass executingFilePath (in your RazorViewToStringRenderer's RenderViewToStringAsync method) to FindView as additional parameter:

var view = FindView(executingFilePath, actionContext, viewName);

I hope it helps.

Related