添加要在运行时可供 Razor 页面使用的程序集/类型

Ric*_*ahl 5 c# razor-pages asp.net-core-3.0

我正在尝试构建一个动态 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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

程序集加载到 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
Run Code Online (Sandbox Code Playgroud)

效果很好。因此,程序集已加载且类型可访问,但 Razor 似乎没有意识到这一点。

所以问题是:

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

r-L*_*rch 2

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

所有 Razor 视图编译开始于:

RuntimeViewCompiler.CreateCompilation (..)

它使用: CSharpCompiler.Create(..,..,引用:..)

它使用: RazorReferenceManager.CompilationReferences

使用:参见 github 上的代码

// simplyfied
var referencePaths = ApplicationPartManager.ApplicationParts
    .OfType<ICompilationReferencesProvider>()
    .SelectMany(_ => _.GetReferencePaths())
Run Code Online (Sandbox Code Playgroud)

它使用: ApplicationPartManager.ApplicationParts

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

应用部分管理器

在搜索应用程序部分时,会执行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.
Run Code Online (Sandbox Code Playgroud)
  1. 然后它循环遍历所有找到的程序集并使用ApplicationPartFactory.GetApplicationPartFactory (程序集)(如第 69 行所示)来查找扩展的类型ApplicationPartFactory

  2. GetApplicationParts(assembly)然后它对所有找到的s调用该方法ApplicationPartFactory

所有没有ApplicationPartFactory得到DefaultApplicationPartFactory返回的程序new AssemblyPart(assembly)GetApplicationParts

public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
Run Code Online (Sandbox Code Playgroud)

获取应用程序零件工厂

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

一种解决方案

这意味着我们可以创建并注册(使用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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我的工作测试设置:

  • 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>
Run Code Online (Sandbox Code Playgroud)
services
   .AddRazorPages()
   .AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.
Run Code Online (Sandbox Code Playgroud)

〜/页面/Index.cshtml

@page

<pre>
    output: [
        @(
            new ClassLibrary1.Class1().Method1()
        )
    ]
</pre>
Run Code Online (Sandbox Code Playgroud)

它显示了预期的输出:

    output: [ 
        Hallo, World!
    ]
Run Code Online (Sandbox Code Playgroud)

祝你今天过得愉快。