在运行时从子文件夹加载带有引用的程序集

7

我正在开发一个项目,作为多个插件的框架,这些插件应该在运行时加载。

我的任务是在应用程序文件夹中有以下结构:

  • 两个目录及其子目录。一个命名为“/addons”用于插件,另一个命名为“/ref”用于任何这些插件可能使用的其他引用(如System.Windows.Interactivity.dll)。
  • 从应用程序菜单中选择其中一个插件后,应在运行时加载.dll并打开预设入口点。
  • 新加载的程序集的所有引用也应该被加载。

我知道加载插件时的子文件夹和文件名,因此我只需使用Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location))Path.Combine()来构建到.dll的路径,然后使用Assembly.LoadFile()进行加载,接着使用反射与assembly.GetExportedTypes()查找继承自我的'EntryPointBase'类的类,最后使用Activator.CreateInstance()创建它。

但是,一旦我在我的Add-On中有任何引用,在assembly.GetExportedTypes()处将弹出一个System.IO.FileNotFoundException

我编写了一个方法来加载所有引用的程序集,甚至使它递归加载所有引用的引用,如下所示:

public void LoadReferences(Assembly assembly)
{

  var loadedReferences = AppDomain.CurrentDomain.GetAssemblies();

  foreach (AssemblyName reference in assembly.GetReferencedAssemblies())
  {
    //only load when the reference has not already been loaded 
    if (loadedReferences.FirstOrDefault(a => a.FullName == reference.FullName) == null)
    {
      //search in all subfolders
      foreach (var location in Directory.GetDirectories(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location)))
      {
        //GetDirectoriesRecusrive searchs all subfolders and their subfolders recursive and 
        //returns a list of paths for all files found
        foreach (var dir in GetDirectoriesRecusrive(location))
        {

          var assemblyPath = Directory.GetFiles(dir, "*.dll").FirstOrDefault(f => Path.GetFileName(f) == reference.Name+".dll");
          if (assemblyPath != null)
          {
            Assembly.LoadFile(assemblyPath); 
            break; //as soon as you find a vald .dll, stop the search for this reference.
          }
        }
      }
    }
  }
}

通过检查AppDomain.CurrentDomain.GetAssemblies(),确保所有参考项都已加载,但异常仍然存在。

如果所有程序集都直接放在应用程序文件夹中,或者如果插件的所有引用项已经由启动应用程序本身引用,则其可以工作。这两种方式都不适合我的情况,因为领导们要求使用此文件系统,并且新引用的插件应能够在不触及应用程序本身的情况下加载。

问题:

如何从一个子文件夹中加载程序集及其引用而不出现System.IO.FileNotFoundException错误?

附加信息:

  • 应用程序采用新的.csproj格式,并在<TargetFrameworks>netcoreapp3.1;net472</TargetFrameworks>上运行,尽管对net472的支持很快将停止(目前仍在net472上进行调试)
  • 大多数插件仍采用旧的.csproj格式,在net472上运行
  • ref子文件夹也是按子文件夹结构构建的(devexpress、system等),而插件子文件夹没有更多的子文件夹。

简而言之,您正在寻找AppDomainAssemblyResolve事件。 - Reza Aghaei
2个回答

9

TL;DR;

你需要寻找AppDomainAssemblyResolve事件。如果你正在当前应用程序域中加载所有插件程序集,则需要处理AppDomain.CurrentDomain的事件并在事件处理程序中加载请求的程序集。

无论你有什么样的引用文件夹结构,你应该执行以下操作:

  • 从插件文件夹获取所有程序集文件
  • 从参考文件夹(整个层次结构)获取所有程序集文件
  • 处理AppDomain.CurrentDomainAssemblyResolve,检查所请求的程序集名称是否在参考文件夹文件中可用,然后加载和返回程序集。
  • 对于插件文件夹中的每个程序集文件,获取所有类型,如果该类型实现了你的插件接口,则实例化它并调用其入口点。

例子

在这个 PoC 中,我在运行时动态地从 Plugins 文件夹中加载所有 IPlugin 的实现,并在加载它们并解决所有依赖关系后,调用插件的 SayHello 方法。

加载插件的应用程序与插件没有任何依赖关系,只是在运行时从以下文件夹结构中加载它们:

enter image description here

这是我为加载、解析和调用插件所做的工作:
var plugins = new List<IPlugin>();
var pluginsPath = Path.Combine(Application.StartupPath, "Plugins");
var referencesPath = Path.Combine(Application.StartupPath, "References");

var pluginFiles = Directory.GetFiles(pluginsPath, "*.dll", 
    SearchOption.AllDirectories);
var referenceFiles = Directory.GetFiles(referencesPath, "*.dll", 
    SearchOption.AllDirectories);

AppDomain.CurrentDomain.AssemblyResolve += (obj, arg) =>
{
    var name = $"{new AssemblyName(arg.Name).Name}.dll";
    var assemblyFile = referenceFiles.Where(x => x.EndsWith(name))
        .FirstOrDefault();
    if (assemblyFile != null)
        return Assembly.LoadFrom(assemblyFile);
    throw new Exception($"'{name}' Not found");
};

foreach (var pluginFile in pluginFiles)
{
    var pluginAssembly = Assembly.LoadFrom(pluginFile);
    var pluginTypes = pluginAssembly.GetTypes()
        .Where(x => typeof(IPlugin).IsAssignableFrom(x));
    foreach (var pluginType in pluginTypes)
    {
        var plugin = (IPlugin)Activator.CreateInstance(pluginType);
        var button = new Button() { Text = plugin.GetType().Name };
        button.Click += (obj, arg) => MessageBox.Show(plugin.SayHello());
        flowLayoutPanel1.Controls.Add(button);
    }
}

这是结果:

enter image description here

您可以下载或克隆代码:


嗨,感谢您的出色回答。详细的步骤说明确实帮助我自己解决了问题。如果您有时间,也能看一下这个问题吗?我发现由于AssemblyResolve事件订阅,我不能再使用DataSet Visualizer了。也许您知道原因。 - Azzarrel
有什么想法为什么它会尝试加载无数的.resources.dll文件?我既没有任何资源,也没有这个名称的文件,但它会尝试正常加载任何程序集一次,然后作为资源加载一次(System.Windows.Interactivity.dll被加载一次作为这个名称,一次作为System.Windows.Interactivity.dll,显然找不到)。 - Azzarrel
那些卫星程序集是用于本地化的。也许你的应用程序或依赖程序集正在使用resX资源,并且有卫星程序集。这些程序集通常应该位于与所请求的区域性相同名称的文件夹中。如果你没有这些程序集,就忽略它们即可。 - Reza Aghaei
我在这个上下文中没有尝试过,但我的理解是它为您提供了本地化的机会。您可以选择接受或忽略它。 - Reza Aghaei

1
Microsoft已经使用他们的Add-in框架解决了这些问题。我建议查看他们的链接Walkthrough: Creating an Extensible Application。该链接包含完整的步骤,从开始到完成创建可扩展控制台应用程序,并解释了各种场景。您可以从特定文件夹加载插件。我怀疑它也会解决您可能遇到的问题。
它还处理向后兼容性问题,例如“新主机,旧插件”,以及自定义插件。
管道场景:新主机,旧插件。

enter image description here

这个管道也在Add-in Pipeline Scenarios中描述。
因此,对于 .Net Core,我们有一种不同的创建可扩展应用程序的方法。以下微软文章Create a .NET Core application with plugins建议使用程序集依赖解析器来处理具有依赖项的插件。

4
该文章的作者包含了 ".net-core" 标签。您提供的内容来自于 2013 年,是针对 .NET Framework 4 的。在 .NET Core 中不包括如 [AddInContract] 这样的引用属性等依赖库。如果这仍然是当前的指导方针,那么应该提供更新的引用链接以解决 .NET Core API 的问题。然而据我所知,这种方法已不再被积极支持用于 .NET Core。 - Jeremy Caney

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接