toddams / RazorLight

Template engine based on Microsoft's Razor parsing engine for .NET Core
Apache License 2.0
1.52k stars 259 forks source link

System.IO.FileNotFoundException. Can't CompileRender when executable is in directory with #(hashtag) in path #355

Closed Basyras closed 4 years ago

Basyras commented 4 years ago

Describe the bug RazorLightEngine.CompileRenderAsync() fails because DefaultMetadataReferenceManager incorrectly formats assembly paths when containing # character.

DefaultMetadataReferenceManager private AssemblyDirectory method incorrectly formats paths and splits path from # character resulting in incorrect path and error when trying to load this path as assembly.

    private static string AssemblyDirectory(Assembly assembly)
    {
        string codeBase = assembly.CodeBase;
        UriBuilder uri = new UriBuilder(codeBase);
        return Uri.UnescapeDataString(uri.Path);
    }

example: assemlby.CodeBase is file:///C:/C#/Myproject uri.Path results to be C:/C and rest of path is stored inuri.fragment#/Myproject

To Reproduce Use .NET Framework 4.6.1 project with RazorLight package. Projects (executable) needs to be in directory with # character in its path (yes manually moving bin folder in new path without hashtag character resolve the issue)

Expected behavior Should CompileRender and return html. DefaultMetadataReferenceManager private AssemblyDirectory method should correctly format path with # character (should not split path in half in position of # character)

Information (please complete the following information):

Additional context Tested in .NET Core 3.0 and this issue is probablly resolved, Iam aware this issue is not problem of RazorLight but little tweak in RazorLight/Compilation/DefaultMetadataReferenceManager.cs as this solves the problem:

    private static string AssemblyDirectory(Assembly assembly)
    {
        string codeBase = assembly.CodeBase;
        UriBuilder uri = new UriBuilder(codeBase);
        return Uri.UnescapeDataString(uri.Path + uri.Fragment); //<-- this line edited
    }

Stack trace: at System.IO.Error.WinIOError(Int32 errorCode, String maybeFullPath) at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share) at RazorLight.Compilation.DefaultMetadataReferenceManager.Resolve(Assembly assembly, DependencyContext dependencyContext) at RazorLight.Compilation.DefaultMetadataReferenceManager.Resolve(Assembly assembly) at RazorLight.Compilation.RoslynCompilationService.EnsureOptions() at RazorLight.Compilation.RoslynCompilationService.get_ParseOptions() at RazorLight.Compilation.RoslynCompilationService.CreateSyntaxTree(SourceText sourceText) at RazorLight.Compilation.RoslynCompilationService.CreateCompilation(String compilationContent, String assemblyName) at RazorLight.Compilation.RoslynCompilationService.CompileAndEmit(IGeneratedRazorTemplate razorTemplate) at RazorLight.Compilation.RazorTemplateCompiler.CompileAndEmit(RazorLightProjectItem projectItem) at RazorLight.Compilation.RazorTemplateCompiler.OnCacheMissAsync(String templateKey) at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at RazorLight.EngineHandler.d15.MoveNext() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at RazorLight.EngineHandler.d191.MoveNext() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at SmlouvyBusiness.Utilities.Controllers.TemplateFiller.GetFamilyHtml(RazorContractModel model) in C:\Honza\Prace\Programovani\C#\Bucek\Smlouvy\Smlouvy\DocumentWorks\Utilities\Controllers\TemplateFiller.cs:line 142 at SmlouvyBusiness.Utilities.Controllers.TemplateFiller.CreataFamilyPDF(ContractModel model) in C:\Honza\Prace\Programovani\C#\Bucek\Smlouvy\Smlouvy\DocumentWorks\Utilities\Controllers\TemplateFiller.cs:line 102 at SmlouvyWPF.Views.Dialogs.CreateContractDialog.<>cDisplayClass23_0.b__0() in C:\Honza\Prace\Programovani\C#\Bucek\Smlouvy\Smlouvy\Smlouvy\Views\Dialogs\CreateContractDialog.xaml.cs:line 136 at System.Threading.Tasks.Task.InnerInvoke() at System.Threading.Tasks.Task.Execute()

Basyras commented 4 years ago

Is there any "dependency injection" way how to inject IMetadataReferenceManager when creating RazorLightEngine perhaps something like this?

engine = new RazorLightEngineBuilder()
              .UseFileSystemProject(templatesDirectory)
              .UseMemoryCachingProvider()
              .UseMetadataReferenceManager<FixedMetadataReferenceManager>()
              .Build()

Anyway ... I have created nasty workaround

Some things were marked as internal or private so i have to copy pasted them.

Usage:

var engine = FixedRazorLightEngineCreator.CreateEngine(); var html = engine.CompileRenderAsync(...);

and create file like this:

using Microsoft.AspNetCore.Mvc.Razor.Extensions;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.DependencyModel;
using RazorLight;
using RazorLight.Caching;
using RazorLight.Compilation;
using RazorLight.Generation;
using RazorLight.Instrumentation;
using RazorLight.Razor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.PortableExecutable;

namespace RazorLight.Extensions
{
    public static class FixedRazorLightEngineCreator
    {
        public static RazorLightEngine CreateEngine(string templatesDirectoryPath, RazorLightOptions options = null, Assembly operatingAssemlby = null)
        {
            if (options == null)
            {
                options = new RazorLightOptions();
                options.CachingProvider = new MemoryCachingProvider();
            }

            operatingAssemlby = operatingAssemlby ?? Assembly.GetEntryAssembly();

            //var metadataReferenceManager = new DefaultMetadataReferenceManager(options.AdditionalMetadataReferences, options.ExcludedAssemblies);
            var metadataReferenceManager = new FixedMetadataReferenceManager(options.AdditionalMetadataReferences,options.ExcludedAssemblies);
            var compiler = new RoslynCompilationService(metadataReferenceManager, operatingAssemlby);
            var razorProject = new FileSystemRazorProject(templatesDirectoryPath);
            var sourceGenerator = new RazorSourceGenerator(DefaultRazorEngine.Instance, razorProject, options.Namespaces);
            var templateCompiler = new RazorTemplateCompiler(sourceGenerator, compiler, razorProject, options);
            var templateFactoryProvider = new TemplateFactoryProvider();
            var engine = new RazorLightEngine(new EngineHandler(options, templateCompiler, templateFactoryProvider, options.CachingProvider));

            return engine;
        }
    }

    internal sealed class DefaultRazorEngine
    {
        public static Microsoft.AspNetCore.Razor.Language.RazorEngine Instance
        {
            get
            {
                var configuration = RazorConfiguration.Default;
                var razorProjectEngine = RazorProjectEngine.Create(configuration, new NullRazorProjectFileSystem(), builder =>
                {
                    RazorLight.Instrumentation.InjectDirective.Register(builder);
                    RazorLight.Instrumentation.ModelDirective.Register(builder);

                    //In RazorLanguageVersion > 3.0 (at least netcore 3.0) the directives are registered out of the box.
                    if (!RazorLanguageVersion.TryParse("3.0", out var razorLanguageVersion)
                        || configuration.LanguageVersion.CompareTo(razorLanguageVersion) < 0)
                    {
                        NamespaceDirective.Register(builder);
                        FunctionsDirective.Register(builder);
                        InheritsDirective.Register(builder);

                    }
                    SectionDirective.Register(builder);

                    builder.Features.Add(new ModelExpressionPass());
                    builder.Features.Add(new RazorLightTemplateDocumentClassifierPass());
                    builder.Features.Add(new RazorLightAssemblyAttributeInjectionPass());
#if NETSTANDARD2_0
                   builder.Features.Add(new InstrumentationPass());
#endif
                    //builder.Features.Add(new ViewComponentTagHelperPass());

                    builder.AddTargetExtension(new TemplateTargetExtension()
                    {
                        TemplateTypeName = "global::RazorLight.Razor.RazorLightHelperResult",
                    });

                    OverrideRuntimeNodeWriterTemplateTypeNamePhase.Register(builder);
                });

                return razorProjectEngine.Engine;
            }
        }

        private class NullRazorProjectFileSystem : RazorProjectFileSystem
        {
            public override IEnumerable<RazorProjectItem> EnumerateItems(string basePath)
            {
                throw new System.NotImplementedException();
            }

#if (NETCOREAPP3_0 || NETCOREAPP3_1)
            [System.Obsolete]
#endif
            public override RazorProjectItem GetItem(string path)
            {
                throw new System.NotImplementedException();
            }

#if (NETCOREAPP3_0 || NETCOREAPP3_1)
            public override RazorProjectItem GetItem(string path, string fileKind)
            {
                throw new System.NotImplementedException();
            }
#endif
        }
    }

    internal class OverrideRuntimeNodeWriterTemplateTypeNamePhase : RazorEnginePhaseBase
    {
        public static void Register(RazorProjectEngineBuilder builder)
        {
            var defaultRazorCSharpLoweringPhase = builder.Phases.SingleOrDefault(x =>
            {
                var type = x.GetType();
                // This type is not public, so we can't use typeof() operator to match to x.GetType() value.
                // Additionally, we can't use Type.GetType("Microsoft.AspNetCore.Razor.Language.DefaultRazorCSharpLoweringPhase, Microsoft.AspNetCore.Razor.Language")
                // because apparently it can fail during Azure Functions rolling upgrades? Per user report: https://github.com/toddams/RazorLight/issues/322
                var assemblyQualifiedNameOfTypeWeCareAbout = "Microsoft.AspNetCore.Razor.Language.DefaultRazorCSharpLoweringPhase, Microsoft.AspNetCore.Razor.Language, ";
                return type.AssemblyQualifiedName.Substring(0, assemblyQualifiedNameOfTypeWeCareAbout.Length) == assemblyQualifiedNameOfTypeWeCareAbout;
            });

            if (defaultRazorCSharpLoweringPhase == null)
            {
                throw new RazorLightException("SetTemplateTypePhase cannot be registered as DefaultRazorCSharpLoweringPhase could not be located");
            }

            // This phase needs to run just before DefaultRazorCSharpLoweringPhase
            var phaseIndex = builder.Phases.IndexOf(defaultRazorCSharpLoweringPhase);
            builder.Phases.Insert(phaseIndex, new OverrideRuntimeNodeWriterTemplateTypeNamePhase("global::RazorLight.Razor.RazorLightHelperResult"));
        }

        private readonly string _templateTypeName;

        public OverrideRuntimeNodeWriterTemplateTypeNamePhase(string templateTypeName)
        {
            _templateTypeName = templateTypeName;
        }

        protected override void ExecuteCore(RazorCodeDocument codeDocument)
        {
            var documentNode = codeDocument.GetDocumentIntermediateNode();
            ThrowForMissingDocumentDependency(documentNode);

            documentNode.Target = new RuntimeNodeWriterTemplateTypeNameCodeTarget(documentNode.Target, _templateTypeName);
        }

        internal class RuntimeNodeWriterTemplateTypeNameCodeTarget : CodeTarget
        {
            private readonly CodeTarget _target;
            private readonly string _templateTypeName;

            public RuntimeNodeWriterTemplateTypeNameCodeTarget(CodeTarget target, string templateTypeName)
            {
                _target = target;
                _templateTypeName = templateTypeName;
            }

            public override IntermediateNodeWriter CreateNodeWriter()
            {
                var writer = _target.CreateNodeWriter();
                if (writer is RuntimeNodeWriter runtimeNodeWriter)
                {
                    runtimeNodeWriter.TemplateTypeName = _templateTypeName;
                }

                return writer;
            }

            public override TExtension GetExtension<TExtension>()
            {
                return _target.GetExtension<TExtension>();
            }

            public override bool HasExtension<TExtension>()
            {
                return _target.HasExtension<TExtension>();
            }
        }
    }

    public class FixedMetadataReferenceManager : IMetadataReferenceManager
    {
        HashSet<MetadataReference> _metadataReferences;
        HashSet<string> _excludedAssemblies;

        public FixedMetadataReferenceManager(HashSet<MetadataReference> metadataReferences = null, HashSet<string> excludedAssemblies = null)
        {
            _metadataReferences = metadataReferences;
            _excludedAssemblies = excludedAssemblies ?? new HashSet<string>();

        }
        public HashSet<MetadataReference> AdditionalMetadataReferences => _metadataReferences;

        public IReadOnlyList<MetadataReference> Resolve(Assembly assembly)
        {
            var dependencyContext = DependencyContext.Load(assembly);

            return Resolve(assembly, dependencyContext);
        }

        private IReadOnlyList<MetadataReference> Resolve(Assembly assembly, DependencyContext dependencyContext)
        {
            var libraryPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            IEnumerable<string> references = null;
            if (dependencyContext == null)
            {
                var context = new HashSet<string>();
                var x = GetReferencedAssemblies(assembly, _excludedAssemblies, context).Union(new Assembly[] { assembly }).ToArray();
                references = x.Select(p => AssemblyDirectory(p));
            }
            else
            {
                references = dependencyContext.CompileLibraries.SelectMany(library => library.ResolveReferencePaths());

                if (!references.Any())
                {
                    throw new RazorLightException("Can't load metadata reference from the entry assembly. " +
                                                  "Make sure PreserveCompilationContext is set to true in *.csproj file");
                }
            }

            var metadataReferences = new List<MetadataReference>();

            foreach (var reference in references)
            {
                if (libraryPaths.Add(reference))
                {
                    using (var stream = File.OpenRead(reference))
                    {
                        var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata);
                        var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata);

                        metadataReferences.Add(assemblyMetadata.GetReference(filePath: reference));
                    }
                }
            }

            if (AdditionalMetadataReferences.Any())
            {
                metadataReferences.AddRange(AdditionalMetadataReferences);
            }

            return metadataReferences;
        }

        private static IEnumerable<Assembly> GetReferencedAssemblies(Assembly a, IEnumerable<string> excludedAssemblies, HashSet<string> visitedAssemblies = null)
        {
            visitedAssemblies = visitedAssemblies ?? new HashSet<string>();
            if (!visitedAssemblies.Add(a.GetName().EscapedCodeBase))
            {
                yield break;
            }

            foreach (var assemblyRef in a.GetReferencedAssemblies())
            {
                if (visitedAssemblies.Contains(assemblyRef.EscapedCodeBase))
                { continue; }

                if (excludedAssemblies.Any(s => s.Contains(assemblyRef.Name)))
                { continue; }
                var loadedAssembly = Assembly.Load(assemblyRef);
                yield return loadedAssembly;
                foreach (var referenced in GetReferencedAssemblies(loadedAssembly, excludedAssemblies, visitedAssemblies))
                {
                    yield return referenced;
                }

            }
        }

        private static string AssemblyDirectory(Assembly assembly)
        {
            string codeBase = assembly.CodeBase;
            UriBuilder uriBuilder = new UriBuilder(codeBase);
            string assemlbyDirectory = Uri.UnescapeDataString(uriBuilder.Path + uriBuilder.Fragment);
            return assemlbyDirectory;

        }

    }
}
jzabroski commented 4 years ago

Hi @Basyras . Is this only on legacy .NET Framework? Any chance you can write a test in the project and add the test ignore property so that it doesnt fail the build, just provides a repro?

jzabroski commented 4 years ago

Hi @Basyras One more note. I recently started to try to make the code more DI composable. See: https://github.com/toddams/RazorLight/commit/cc9ab9674179c8f3d981704889474946df57df76 and https://github.com/toddams/RazorLight/commit/5642e7be11121004fe400d26fe451d366f42db5d - but I dont believe this is released yet. Do you think these would simplify your workaround?

Basyras commented 4 years ago

Hi @Basyras . Is this only on legacy .NET Framework? Any chance you can write a test in the project and add the test ignore property so that it doesnt fail the build, just provides a repro?

@jzabroski The error is not present in .net core 2 when beta versions are used plus stable RazorLight version (1.1) doesn't have this issue either even when used in .NET framework 4.6.1 (can't use this version anymore because after upgrading reference system to ".net core-ish" i get issue like this one https://github.com/toddams/RazorLight/issues/69 )

Hi @Basyras One more note. I recently started to try to make the code more DI composable. See: cc9ab96 and 5642e7b - but I dont believe this is released yet. Do you think these would simplify your workaround?

changing AddSingleton to TryAddSingleton is good thing but in my case it doesn't do much. I still don't have access to change DefaultMetadataReferenceManager? Because the RazorLightEngine is not created by Dependency System because it is created in Func<IRazorLightEngine> parameter of AddRazorLight(this IServiceCollection,Func<IRazorLightEngine> delegate)

If you don't mind i will create new feature request that will describe how i would like to tweak the AddRazorLight() extension to be as you said more DI composable

jzabroski commented 4 years ago

Awesome, thanks

On Fri, Jul 24, 2020, 2:42 PM Basyras notifications@github.com wrote:

Hi @Basyras https://github.com/Basyras . Is this only on legacy .NET Framework? Any chance you can write a test in the project and add the test ignore property so that it doesnt fail the build, just provides a repro?

@jzabroski https://github.com/jzabroski The error is not present in .net core 2 when beta versions are used plus stable RazorLight version (1.1) doesn't have this issue either even when used in .NET framework 4.6.1 (can't use this version anymore because after upgrading reference system to ".net core-ish" i get issue like this one #69 https://github.com/toddams/RazorLight/issues/69 )

Hi @Basyras https://github.com/Basyras One more note. I recently started to try to make the code more DI composable. See: cc9ab96 https://github.com/toddams/RazorLight/commit/cc9ab9674179c8f3d981704889474946df57df76 and 5642e7b https://github.com/toddams/RazorLight/commit/5642e7be11121004fe400d26fe451d366f42db5d

  • but I dont believe this is released yet. Do you think these would simplify your workaround?

changing AddSingleton to TryAddSingleton is good thing but in my case it doesn't do much. I still don't have access to change DefaultMetadataReferenceManager? Because the RazorLightEngine is not created by Dependency System because it is created in Func parameter of AddRazorLight(this IServiceCollection,Func delegate)

If you don't mind i will create new feature request that will describe how i would like to tweak the AddRazorLight() extension to be as you said more DI composable

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/toddams/RazorLight/issues/355#issuecomment-663679356, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADNH7JG4IHI7V23AQTZLA3R5HI25ANCNFSM4PGTBJSA .

Basyras commented 4 years ago

@jzabroski I was bored and kind of changed my mind so i created pull request with default implementation of my "dreamed" DI Extension

jzabroski commented 4 years ago

I like dreams. Will review it this weekend or Monday at latest. Thanks for your help.

jzabroski commented 4 years ago

@Basyras I think we can close this now that beta10 is shipped, yes? I do need to create release notes tho

Basyras commented 4 years ago

@jzabroski Ok

jzabroski commented 4 years ago

Thanks again for your help!

Basyras commented 4 years ago

Glad to help ❤️