toddams/RazorLight

Cannot reconstruct the original paths of the embedded files, preventing debugging from working.

ascott18 opened this issue · 3 comments

Describe the bug
In RazorSourceGenerator, RazorSourceDocument.ReadFrom is called with projectItem.Key as the fileName. When an embedded resource exists in a nested directory in the project (which, it almost always does), this key looks something like "Views.File.cshtml". Unfortunately, this then prevents the debugger from correlating the generated C# to the original source file on disk.

If the file name were to be passed as the actual file name on disk (directory is not needed, e.g. just "File.cshtml"), the debugger works as expected and you can set breakpoints in your templates.

To Reproduce
Steps to reproduce the behavior:

  1. Add a cshtml file in a non-project-root directory.
  2. Make an engine:
        var engine = new RazorLightEngineBuilder()
               .UseEmbeddedResourcesProject(typeof(Root))
               .SetOperatingAssembly(typeof(Root).Assembly)
               .UseMemoryCachingProvider()
               .EnableDebugMode()
               .Build();
  1. Put a breakpoint in the cshtml file.
  2. Execute the template. string result = await engine.CompileRenderAsync<object>("Views.File", model);

Expected behavior
Breakpoint is hit.

Information (please complete the following information):

  • OS: [ e.g Ubuntu 16.04, Windows Server 2016, etc ] Windows 10
  • Platform [.NET Framework 4.x / .NET Core 2.x , etc] .NET 6
  • RazorLight version [ e.g 2.0-beta1 ] 2.3.0
  • Are you using the OFFICIAL RazorLight package? https://www.nuget.org/packages/razorlight yes
  • Visual Studio version [ e.g Visual Studio Community 17.8.5 ] 17.4.0

Additional context

I realize that its impossible to 100% correctly deduce the filename from an embedded resource because directory slashes become dots. What would be nice is an additional, overridable property FileName on RazorLightProjectItem whose default implementation is Key, and on EmbeddedRazorProjectItem its implemention is something like string.Join('.', Key.Split(".").Reverse().Take(2).Reverse()) (i.e. assume that the file name on disk contains no dots other than the extension, which will be correct most of the time and at least be more correct than it is now). Custom implementations can then create their own EmbeddedRazorProject implementation and set the FileName according to their own logic, if needed.

This is our workaround for this, which is the closest I can get without a dedicated FileName property that is used by RazorSourceGenerator. This does make debugging function correctly:

var engine = new RazorLightEngineBuilder()
               .UseProject(new NameFixingRazorLightProject(typeof(Root)))
               .SetOperatingAssembly(typeof(Root).Assembly)
               .UseMemoryCachingProvider()
               .EnableDebugMode()
               .Build();

class NameFixingRazorLightProject : EmbeddedRazorProject
{
    public NameFixingRazorLightProject(Type rootType) : base(rootType) { }

    private ConcurrentDictionary<string, string> _KeyMapping = new();

    public override async Task<RazorLightProjectItem> GetItemAsync(string templateKey)
    {
        var item = new CorrectlyNamedRazorLightProjectItem(await base.GetItemAsync(templateKey));

        if (!_KeyMapping.TryAdd(item.Key, item.OriginalKey) && _KeyMapping.TryGetValue(item.Key, out string? conflictingKey) && conflictingKey != item.OriginalKey)
        {
            throw new InvalidOperationException(
                "Duplicate cshtml file template names detected. For this workaround for debugger file names, " +
                "all cshtml files must have unique file names (directory does not contribute to uniqueness)." +
                $"Duplicate files: {item.OriginalKey}, {conflictingKey}"
            );
        }

        return item;
    }
}

class CorrectlyNamedRazorLightProjectItem : RazorLightProjectItem
{
    public CorrectlyNamedRazorLightProjectItem(RazorLightProjectItem baseItem)
    {
        BaseItem = baseItem;
    }

    public RazorLightProjectItem BaseItem { get; }

    // RazorLight uses the key as the filename when compiling the cshtml template,
    // and if this key doesn't match the name of the file on disk, the debugger won't be able
    // to correlate the PDB to the source file.
    // Normally, this key looks something like "Views.File.cshtml", but we need it to look like "File.cshtml".
    public override string Key => string.Join('.', BaseItem.Key.Split(".").Reverse().Take(2).Reverse());

    public string OriginalKey => BaseItem.Key;
    public override bool Exists => BaseItem.Exists;
    public override Stream Read() => BaseItem.Read();
}

I am not going to consider this. I am sorry that this happened to you, and happy for you that you found a workaround.

Directory names can and do contribute to uniqueness. If you need a way to debug embedded templates, I suggest using file system projects instead. There is no such thing as debugging embedded resources anywhere in .NET. If you are using .NET Core, you can use the File Provider API and your code will be less odd than the one you proposed: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-7.0#manifest-embedded-file-provider

If you simplify this approach using the manifest embedded file provider, perhaps I will consider a PR, but this initial post has me quite skeptical.

Thanks for the follow-up. I'm checking with my team to see if there's a specific reason why embedded templates were used in this case rather than pulling from the filesystem, as I didn't write the original implementation.

Thanks again for taking the time to look into this. There was indeed no reason for the project to being using manifest embedded templates rather than filesystem-sourced templates.