toddams/RazorLight

Unable to read cshtml from compiled project.

Opened this issue ยท 14 comments

I'm unable to read the cshtml from a compiled dll that net core 3.1 creates when deploy the app.

To Reproduce

string assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
var metadataReference = Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(Path.Combine(assemblyFolder, "Project.Views.dll"));
                var engine = new RazorLightEngineBuilder()
                 .UseMemoryCachingProvider()
                 .UseEmbeddedResourcesProject(typeof(Program))
                 .AddMetadataReferences(metadataReference)
                 .Build();

string html = await engine.CompileRenderAsync<object>("Views.CV._TheView", Model);

Expected behavior
HTML of view

but get message: Project can not find template with key ....

What do I missing?

What version are you using and what's your project structure look like?

Is Project.Views a separate project/assembly where your .cshtml files are located? If so, I believe you need to pass a type that exists at the root of your Project.Views project to UseEmbeddedResourcesProject (Or you use the other signature: UseEmbeddedResourcesProject(Assembly assembly, string rootNamespace = null) ).

For example: .UseEmbeddedResourcesProject(typeof(Views.Root)) where Root is simply an empty class. Then when compiling/rendering, you would use string html = await engine.CompileRenderAsync("CV._TheView", model);.

Here's a working (for me at least) example:

Structure:

Project/
  Model.cs
  Program.cs
  Project.csproj
Project.Views/
  Project.Views.csproj
  Root.cs
  CV/
    _TheView.cshtml

Code:

string assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
var metadataReference = Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(Path.Combine(assemblyFolder, "Project.Views.dll"));
var engine = new RazorLightEngineBuilder()
  .UseMemoryCachingProvider()
  .UseEmbeddedResourcesProject(typeof(Views.Root))
  .AddMetadataReferences(metadataReference)
  .Build();

var model = new Model();
string html = await engine.CompileRenderAsync("CV._TheView", model);

This also works for me:

var engine = new RazorLightEngineBuilder()
        .UseEmbeddedResourcesProject(typeof(Views.Root))
        .UseMemoryCachingProvider()
        .Build();

string html = await engine.CompileRenderAsync("CV._TheView", new Model());

@maxbanas I wonder if we should add a debug mode that dumps all known keys as part of the error message. So the Exception should be TemplateNotFoundException and has a List KnownTemplateKeys, which is passed by the Razor Project System.

One thing I do in similar projects (like FluentMigrator) is I create an EmbeddedResourceUtility that computes the longest subsequence match and/or Levenshtein distance for all known embedded resource paths vs. the one being searched for.

@maxbanas I wonder if we should add a debug mode that dumps all known keys as part of the error message. So the Exception should be TemplateNotFoundException and has a List KnownTemplateKeys, which is passed by the Razor Project System.

@jzabroski I think something like that would be super helpful. Even though it's pretty straightforward, I've messed up the template keys multiple times and have had to come back and check the readme (especially when the templates are outside of my main project). So I could definitely see how this could be a roadblock for some people just starting out.

@jzabroski I've been thinking about this a bit. If we're on the same page, I can work on a pull request.

To build the exception, we would need RazorLightOptions to determine the debug mode (and include any dynamic template keys?), and a list of known template keys specific to a project type, which I assume would be implemented in RazorLightProject (something like IEnumerable<string> GetKnownTemplateKeys() ).

I see that TemplateNotFoundException is thrown in two places:

  1. RazorTemplateCompiler
  2. RazorSourceGenerator

RazorTemplateCompiler has both a RazorLightProject and a RazorLightOptions so building the exception here is no problem.

In RazorSourceGenerator.GenerateCodeAsync however, a RazorLightProject is optional, and we have no RazorLightOptions. The more I look at this I wonder if an InvalidOperationException or ArgumentException would be more appropriate, since we're not actually looking for the key here, "existing" is just a precondition. And in fact, RazorSourceGenerator.CreateCodeDocumentAsync throws an InvalidOperationException for the same check. But if changing the exception type isn't something we want to do, we could always change the message to be clear why there are no KnownTemplateKeys on the exception.

@maxbanas Yes, I think this also links back to the other discussion we had yesterday about TextSourceRazorProjectItem. https://github.com/toddams/RazorLight/blob/badbd178564ba59934368161c7c140fa5906eee1/src/RazorLight/Razor/TextSourceRazorProjectItem.cs

My point being, children don't know who their parents are. Project Items don't know who their Project is. As a result, information about the parent is unavailable from a child only context.

It looks like you're preparing an rc. I haven't had time to fully implement the "debug mode" and exception message changes, but if the exception type thrown in RazorSourceGenerator.GenerateCodeAsync(RazorLightProjectItem projectItem) is going to change, it might be good to get those into the next major version. I've opened pr #391

@maxbanas The rc suffix is mainly due to the fact we screwed up the semver 2.0.0 naming convention. It was the only way to get the nuget gallery to display the latest package at the top. Most people didnt download the beta10 release because of this mistake. I changing it to use correct dotted identifiers convention of semver 2.0.0 and had to pick an alphabetical identifier higher than the word "beta" in order to fix the sorting on nuget.org

@maxbanas I've tried your solution, but the problem persists. I still got "TemplateNotFoundException" when I try to read a cshtml file from a Razor Class Library.

This is my sample project: https://github.com/knuxbbs/RazorLightSample

@knuxbbs fixing the following issues in your sample project worked for me:

  1. The template key is wrong. It should be "Areas.MyFeature.Pages.Page1", not "RazorClassLibrary.MyFeature.Pages.Page1".
  2. The Build Action for Page1.cshtml is "Content". It should be "Embedded Resource"
  3. It looks the @page directive in Page1.cshtml causes issues. I just removed that line.

As a side note, I ran into the same issue as you when trying to run your example, and didn't really look into that issue. So instead I just built the razor engine in your controller's Get() method, rather than injecting it.
I also had to add this to RazorLightSample.csproj:

<PropertyGroup>
  <PreserveCompilationReferences>true</PreserveCompilationReferences>
  <PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>

@jzabroski I've started working on the feature we discussed and created a new issue, #398, so we can stay on topic (and close this issue if you want).

@georgeemr Can you try RazorLight-2.0.0-rc.3? I plan one last fix before official release.

Sure, sorry for the late response.

To Reproduce

string assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
var metadataReference = Microsoft.CodeAnalysis.MetadataReference.CreateFromFile(Path.Combine(assemblyFolder, "Project.Views.dll"));
                var engine = new RazorLightEngineBuilder()
                 .UseMemoryCachingProvider()
                 .UseEmbeddedResourcesProject(typeof(Program))
                 .AddMetadataReferences(metadataReference)
                 .Build();

string html = await engine.CompileRenderAsync<object>("Views.CV._TheView", Model);

I think the error you ran into was what I ran into in creating a cross-platform build.

Try string assemblyFolder = AppContext.BaseDirectory; instead of string assemblyFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);

For example, this can happen if your assembly was built on Windows but used on MAC OS X.