添加程序集/类型以使其在运行时可用于Razor Page

我正在尝试构建一个动态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()中,我可以-使用Reflection和Type.GetType("namespace.class,assembly")-访问类型。

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

添加程序集/类型以使其在运行时可用于Razor Page

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

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

,效果很好。因此,程序集已加载并且可以访问类型,但是Razor似乎不知道它。

所以问题是:

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

xh_tong 回答:添加程序集/类型以使其在运行时可用于Razor Page

对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. 然后在所有找到的GetApplicationParts(assembly)上调用方法ApplicationPartFactory

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

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实现,然后在{{ 1}}。

GetReferencePaths

我的工作测试设置:

  • ASP.NET Core 3 WebApplication
  • ASP.NET Core 3 ClassLibrary
  • 两个项目彼此无关联
[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);
        }
    }
}
<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>

〜/ Pages / Index.cshtml

services
   .AddRazorPages()
   .AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(@"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.

它显示了预期的输出:

@page

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

今天愉快。

,

事实证明,解决方案是通过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);

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

  

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

本文链接:https://www.f2er.com/3169593.html

大家都在问