Load assemblies with references from subfolders at runtime

一笑奈何 提交于 2020-05-13 05:59:32

问题


I am currently working on a project which is supposed to work as a framework for several Add-Ons, which should be loaded at runtime.

I am tasked to have following structure in my application folder:

  • 2 Directories with subfolders. One named "/addons" for Add-Ons and one named "/ref" for any additonal references these addons might use (like System.Windows.Interactivity.dll)
  • When selecting one of the Add-Ons from a menu in the applicaiton, the .dll is supposed to be loaded at runtime and a pre-set entry point should be opened
  • All references of the newly loaded assembly should be loaded aswell.

I know the subfolder and filename when an Add-On is being loaded, so I simply use Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location)) and Path.Combine() to build a path to the .dll and then load it via Assembly.LoadFile() before using reflection with assembly.GetExportedTypes() to find the class that inherits for my 'EntryPointBase' and then create it with Activator.CreateInstance().

However, as soon as I have any references within my Add-On, an System.IO.FileNotFoundException targeting the reference will pop up at assembly.GetExportedTypes()

I built a method to load all referenced assemblies, even made it recursive to load all references from the references, like this:

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.
          }
        }
      }
    }
  }
}

and made sure all references are loaded in by checking AppDomain.CurrentDomain.GetAssemblies(), but the exception stays the same.

It works if either all assemblies are directly in the application folder, or if all references fro the addon are already referenced by the startup application itself. Both ways are not suitable for my case, because higher ups demand on this file system and Add-Ons with new references should be able to load without touching the appication itself.

Question:

How can I load assemblies from one subfolder and their references from another without a System.IO.FileNotFoundException?

Additional information:

  • Application is in the new .csproj format and runs on <TargetFrameworks>netcoreapp3.1;net472</TargetFrameworks>, although support for net472 should be ceased soon (currently still debugging in net472)
  • Most Add-Ons still have the old .csproj format on net472
  • the ref subfolder is sturctured in subfolders aswell (devexpress, system, etc.), while the addon subfolder has no further subfolders.

回答1:


TL;DR;

You are looking for AssemblyResolve event of the AppDomain. If you are loading all the plugin assemblies in the current app domain, then you need to handle the event for AppDomain.CurrentDomain and load the requested assembly in the event handler.

No matter what folder structure you have for references, what you should do is:

  • Get all assembly files form plugins folder
  • Get all assembly files from references folder (entire hierarchy)
  • Handle AssemblyResolve of AppDomain.CurrentDomain and check if the requested assembly name is available files of reference folder, then load and return assembly.
  • For each assembly file in plugins folder, get all types and if the type implements your plugin interface, instantiate it and call its entry point for example.

Example

In this PoC I load all implementations of IPlugin dynamically at run-time from assemblies in Plugins folder and after loading them and resolving all dependencies at run-time, I call SayHello method of plugins.

The application which loads plugins, doesn't have any dependency to plugins and just loads them at run-time from the following folder structure:

This is what I did for loading, resolving and calling the plugins:

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);
    }
}

And this is the result:

You can download or clone the code:

  • Clone r-aghaei/loadpluginassembly
  • Download zip file



回答2:


Microsoft have already solved these kinds of problems using their Add-in framework. I would recommend looking at their link Walkthrough: Creating an Extensible Application. The link contains a full walk-through to create an extensible console application from start to finish with various senarios explained. You can load add-ins from specific folders. I suspect that it will also resolve the issues that you might have.

It also handles backward compatibility issues such as "New host, old add-ins" and also custom add-ins.

Pipeline scenario: new host, old add-ins.

This pipeline is also described in Add-in Pipeline Scenarios.

So for .Net Core we have a different way of creating an extensible application. The following Microsoft article Create a .NET Core application with plugins suggests to use the assembly dependency resolver for plugins with dependencies.



来源:https://stackoverflow.com/questions/60116804/load-assemblies-with-references-from-subfolders-at-runtime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!