5

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.

KainWhite
  • 101
  • 2
  • 8
  • Please create a [mre]. Assuming `_viewEngine` is a Razor view engine, as far as I remember, you need to leave off the file extension (`.cshtml` here) when searching a view. So I don't believe that this works in another project. – CodeCaster Jul 14 '20 at 17:06
  • @CodeCaster as it is metioned [here](https://github.com/aspnet/Mvc/issues/6676#issuecomment-323146809) there is no need to leave extension off. So it works. – KainWhite Jul 14 '20 at 17:10
  • @KainWhite What happens if you set `isMainPage: false);` in your call to `GetView`? It looks like in `FindView` you just pass the name of the View without the need for directory info, I imagine this method doesn't need the extension. – Ryan Wilson Jul 14 '20 at 17:14
  • @RyanWilson still doesn't find it. Same for `.FindView()` – KainWhite Jul 14 '20 at 17:20
  • @KainWhite Have you tried looking up any other views to see if you can find them? If you can't find other views as well, I imagine there is something wrong with your code. – Ryan Wilson Jul 14 '20 at 17:23
  • Yes, i have. It doesn't find any of them. – KainWhite Jul 14 '20 at 17:25
  • @KainWhite Is this failing locally or on the server? – Ryan Wilson Jul 14 '20 at 17:26
  • @RyanWilson Both locally and on the server. – KainWhite Jul 14 '20 at 17:28
  • @KainWhite You can try reading through this related post (https://stackoverflow.com/questions/56503957/asp-net-core-cannot-find-cshtml-file-by-path-using-razorviewengine) – Ryan Wilson Jul 14 '20 at 17:32
  • @RyanWilson ok, i'll try to do what is written there. But what could have happened just from moving from .Net Core 2.2 to 3.1? – KainWhite Jul 14 '20 at 17:44
  • @RyanWilson solution in the related post didn't help: change startup to configure `RazorViewEngineOptions` with adding both `/Views/{0}` and `/Views/{0}/{1}` + `RazorViewEngine.ViewExtension` - no result, still can't find – KainWhite Jul 14 '20 at 18:18
  • @KainWhite Can you modify your post to show more of your project directory? – Ryan Wilson Jul 14 '20 at 18:32
  • @RyanWilson done – KainWhite Jul 14 '20 at 19:33
  • If your view location ends with "Views" and the view name starts with "Views" as well, I can see what's going wrong. – CodeCaster Jul 15 '20 at 11:18
  • @RyanWilson I'm not sure i got what u said. If you have comprehended what's going wrong here, can you point it out? – KainWhite Jul 15 '20 at 13:56
  • @KainWhite Since you are trying to find the View in your Business project and the Views are contained in your API project, I'd say that is why it's failing to find them. There are ways to do a multi-project search for a View, but it sounds like a pain, to be honest, why not move those Views into your Business Project if that is where they are going to be served from? IF you don't want to go that route, here is a related post which talks about doing a multi-project View search (https://stackoverflow.com/questions/24341336/is-it-possible-to-access-mvc-views-located-in-another-project) – Ryan Wilson Jul 15 '20 at 14:48
  • @RyanWilson ok, but this doesn't explain why it works in another .Net Core 2.2 project. It has the same code and the same structure in it, calling `FindView` from `Business`, while `Views` are in `Api`, it does find those views. – KainWhite Jul 15 '20 at 15:40
  • Maybe i should have mentioned, that both projects are copying this Views folder to `Api\bin\Debug\netcoreapp{version here}` folder on build. – KainWhite Jul 15 '20 at 15:44
  • @KainWhite Did you ever found a solution for this issue? – Goca Jan 04 '23 at 02:15

3 Answers3

1

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);
// ...

        }
RobC
  • 1,303
  • 3
  • 15
  • 32
1

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.

nickornotto
  • 1,946
  • 4
  • 36
  • 68
0

I chased this error from an upgrade I did of an older HTML writer I'd used

In the older version, I used the entire path to the view - the program couldn't find it. I shortened this to simply $"/Views/Home/Page.cshtml", which the IRazorViewEngine had no problem with

TheWizardOfTN
  • 161
  • 10