【问题标题】:Adding Assemblies/Types to be made available to Razor Page at Runtime添加要在运行时对 Razor 页面可用的程序集/类型
【发布时间】:2020-02-29 07:53:54
【问题描述】:

我正在尝试构建一个动态 Web 界面,我可以在其中动态地指向一个文件夹并使用 ASP.NET Core 从该文件夹中提供 Web 内容。通过使用 ASP.NET Core 中的 FileProviders 重新路由 Web 根文件夹,这很容易实现。这适用于 StaticFiles 和 RazorPages。

但是,对于 RazorPages,问题在于一旦执行此操作,您就无法为其他类型动态添加引用。我希望能够选择添加一个文件夹(PrivateBin),在启动时我可以循环访问,加载程序集,然后让这些程序集在 Razor 中可见。

不幸的是,它不起作用,因为即使使用运行时编译,Razor 似乎也看不到加载的程序集。

我在启动期间使用以下内容来加载程序集。请注意,加载这些文件的文件夹不在默认的 ContentRoot 或 WebRoot 中,而是在新的重定向 WebRoot 中。

// WebRoot is a user chosen Path here specified via command line --WebRoot c:\temp\web
private void LoadPrivateBinAssemblies()
{
    var binPath = Path.Combine(WebRoot, "PrivateBin");
    if (Directory.Exists(binPath))
    {
        var files = Directory.GetFiles(binPath);
        foreach (var file in files)
        {
            if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
               !file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
                continue;

            try
            {
                var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
                Console.WriteLine("Additional Assembly: " + file);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to load private assembly: " + file);
            }
        }
    }
}

程序集加载到 AssemblyLoadContext() 中,我可以 - 使用反射和 Type.GetType("namespace.class,assembly") - 访问类型。

但是,当我尝试访问 RazorPages 中的类型时 - 即使启用了运行时编译 - 类型也不可用。我收到以下错误:

为了确保该类型确实可用,我检查了我是否可以在 Razor 中执行以下操作:

@{
 var md = Type.GetType("Westwind.AspNetCore.Markdown.Markdown,Westwind.AspNetCore.Markdown");
 var mdText = md.InvokeMember("Parse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null,
                    null, new object[] { "**asdasd**", false, false, false });
}
@mdText

而且效果很好。所以程序集被加载并且类型是可访问的,但 Razor 似乎没有意识到这一点。

所以问题是:

是否可以在运行时加载程序集并通过运行时编译使它们可用于 Razor,并像通常通过直接声明访问使用类型一样使用它?

【问题讨论】:

  • 看起来 RazorPage 编译选项有 opt.AdditionalReferencePaths 的选项。这让我更接近,但目前仍然无法加载......

标签: c# razor-pages asp.net-core-3.0


【解决方案1】:

事实证明,解决方案是通过 Razor 运行时编译选项,它允许添加额外的“ReferencePaths”,然后显式加载程序集。

在 ConfigureServices() 中:

services.AddRazorPages(opt => { opt.RootDirectory = "/"; })
    .AddRazorRuntimeCompilation(
        opt =>
        {

            opt.FileProviders.Add(new PhysicalFileProvider(WebRoot));
            LoadPrivateBinAssemblies(opt);
        });

然后:

private void LoadPrivateBinAssemblies(MvcRazorRuntimeCompilationOptions opt)
{
    var binPath = Path.Combine(WebRoot, "PrivateBin");
    if (Directory.Exists(binPath))
    {
        var files = Directory.GetFiles(binPath);
        foreach (var file in files)
        {
            if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
               !file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
                continue;

            try
            {
                var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
                opt.AdditionalReferencePaths.Add(file);           
            }
            catch (Exception ex)
            {
                ...
            }

        }
    }

}

关键是:

opt.AdditionalReferencePaths.Add(file);  

这使得程序集对 Razor 可见,但实际上并未加载它。要加载它,您必须显式加载它:

AssemblyLoadContext.Default.LoadFromAssemblyPath(file);

从路径加载程序集。请注意,此程序集必须在应用程序的启动路径或您从中加载的同一文件夹中可用的任何依赖项。

注意:依赖项的加载顺序在这里可能很重要,或者以前未添加的程序集可能找不到作为依赖项(未经测试)。

【讨论】:

    【解决方案2】:

    快速查看 ASP.NET Core 源代码会发现:

    所有 Razor 视图编译开始于:

    RuntimeViewCompiler.CreateCompilation(..)

    它使用: CSharpCompiler.Create(.., .., 参考:..)

    它使用: RazorReferenceManager.CompilationReferences

    使用:see code on github

    // simplyfied
    var referencePaths = ApplicationPartManager.ApplicationParts
        .OfType<ICompilationReferencesProvider>()
        .SelectMany(_ => _.GetReferencePaths())
    

    它使用: ApplicationPartManager.ApplicationParts

    所以我们需要以某种方式注册我们自己的ICompilationReferencesProvider,这就是..

    ApplicationPartManager

    在搜索应用程序部件时,ApplicationPartManager 做了一些事情:

    1. 它搜索隐藏的程序集读取属性,例如:
    [assembly: ApplicationPartAttribute(assemblyName:"..")] // Specifies an assembly to be added as an ApplicationPart
    [assembly: RelatedAssemblyAttribute(assemblyFileName:"..")] // Specifies a assembly to load as part of MVC's assembly discovery mechanism.
    // plus `Assembly.GetEntryAssembly()` gets added automaticly behind the scenes.
    
    1. 然后它遍历所有找到的程序集并使用 ApplicationPartFactory.GetApplicationPartFactory(assembly) (as seen in line 69) 查找扩展 ApplicationPartFactory 的类型。

    2. 然后它在所有找到的ApplicationPartFactorys 上调用方法GetApplicationParts(assembly)

    所有没有ApplicationPartFactory 的程序集都得到DefaultApplicationPartFactory,它在GetApplicationParts 中返回new AssemblyPart(assembly)

    public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
    

    GetApplicationPartFactory

    ​​>

    GetApplicationPartFactory 搜索 [assembly: ProvideApplicationPartFactory(typeof(SomeType))] 然后它使用 SomeType 作为工厂。

    public abstract class ApplicationPartFactory {
    
        public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
    
        public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
        {
            // ...
    
            var provideAttribute = assembly.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>();
            if (provideAttribute == null)
            {
                return DefaultApplicationPartFactory.Instance; // this registers `assembly` as `new AssemblyPart(assembly)`
            }
    
            var type = provideAttribute.GetFactoryType();
    
            // ...
    
            return (ApplicationPartFactory)Activator.CreateInstance(type);
        }
    }
    

    一个解决方案

    这意味着我们可以创建和注册(使用ProvideApplicationPartFactoryAttribute)我们自己的ApplicationPartFactory,它返回一个自定义的ApplicationPart实现,它实现了ICompilationReferencesProvider,然后在GetReferencePaths中返回我们的引用。

    [assembly: ProvideApplicationPartFactory(typeof(MyApplicationPartFactory))]
    
    namespace WebApplication1 {
        public class MyApplicationPartFactory : ApplicationPartFactory {
            public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly)
            {
                yield return new CompilationReferencesProviderAssemblyPart(assembly);
            }
        }
    
        public class CompilationReferencesProviderAssemblyPart : AssemblyPart, ICompilationReferencesProvider {
            private readonly Assembly _assembly;
    
            public CompilationReferencesProviderAssemblyPart(Assembly assembly) : base(assembly)
            {
                _assembly = assembly;
            }
    
            public IEnumerable<string> GetReferencePaths()
            {
                // your `LoadPrivateBinAssemblies()` method needs to be called before the next line executes!
                // So you should load all private bin's before the first RazorPage gets requested.
    
                return AssemblyLoadContext.GetLoadContext(_assembly).Assemblies
                    .Where(_ => !_.IsDynamic)
                    .Select(_ => new Uri(_.CodeBase).LocalPath);
            }
        }
    }
    

    我的工作测试设置:

    • ASP.NET Core 3 Web 应用程序
    • ASP.NET Core 3 类库
    • 两个项目彼此之间没有参考
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <Content Remove="Pages\**" />
      </ItemGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
      </ItemGroup>
    
    </Project>
    
    services
       .AddRazorPages()
       .AddRazorRuntimeCompilation();
    AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\path\to\ClassLibrary1.dll");
    // plus the MyApplicationPartFactory and attribute from above.
    

    ~/Pages/Index.cshtml

    @page
    
    <pre>
        output: [
            @(
                new ClassLibrary1.Class1().Method1()
            )
        ]
    </pre>
    

    它显示了预期的输出:

        output: [ 
            Hallo, World!
        ]
    

    祝你有美好的一天。

    【讨论】:

    • 感谢您提供详细信息 - 这对将来的参考非常有用。事实证明,通过AdditionalReferencePaths() 和手动加载程序集(作为单独的响应发布)有一个内置的解决方案。
    猜你喜欢
    • 2016-05-16
    • 2021-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-18
    • 2021-06-16
    相关资源
    最近更新 更多