在运行时使用子文件夹中的引用加载程序集
Load assemblies with references from subfolders at runtime
我目前正在开发一个项目,该项目应该作为多个附加组件的框架工作,这些附加组件应该在运行时加载。
我的任务是在我的应用程序文件夹中具有以下结构:
- 2 个包含子文件夹的目录。一个名为“/addons”用于附加组件,另一个名为“/ref”用于这些附加组件可能使用的任何附加引用(如
System.Windows.Interactivity.dll
)
- 当从应用程序的菜单中选择一个附加组件时,应该在运行时加载 .dll 并且应该打开一个预设的入口点
- 新加载程序集的所有引用也应加载。
我知道加载项加载时的子文件夹和文件名,所以我只是使用 Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location))
和 Path.Combine()
构建 .dll 的路径,然后通过 [= 加载它14=] 在使用 assembly.GetExportedTypes()
的反射找到为我的 'EntryPointBase' 继承的 class 之前,然后用 Activator.CreateInstance()
.
创建它
但是,只要我的附加组件中有任何 引用 ,就会在 assembly.GetExportedTypes()
[= 处弹出针对引用的 System.IO.FileNotFoundException
22=]
我构建了一个方法来加载所有引用的程序集,甚至使其递归加载引用中的所有引用,如下所示:
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 中调试)
- 大多数附加组件在 net472 上仍然采用旧的 .csproj 格式
- ref 子文件夹也包含子文件夹(devexpress、system 等),而 addon 子文件夹没有其他子文件夹。
TL;DR;
您正在寻找 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
并在事件处理程序中加载请求的程序集。
无论你有什么样的文件夹结构供参考,你应该做的是:
- 从插件文件夹中获取所有程序集文件
- 从引用文件夹(整个层次结构)获取所有程序集文件
- 处理
AppDomain.CurrentDomain
的 AssemblyResolve
并检查请求的程序集名称是否为引用文件夹的可用文件,然后加载和 return 程序集。
- 对于插件文件夹中的每个程序集文件,获取所有类型,如果该类型实现了您的插件接口,则将其实例化并调用其入口点。
例子
在此 PoC 中,我在 运行 时从 Plugins
文件夹中的程序集动态加载 IPlugin
的所有实现,并在加载它们并解析 运行 中的所有依赖项之后-时间,我调用插件的SayHello
方法。
加载插件的应用程序对插件没有任何依赖性,只是在 运行 时从以下文件夹结构加载它们:
这是我加载、解析和调用插件所做的:
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);
}
}
这是结果:
您可以下载或克隆代码:
Microsoft 已经使用其 Add-in 框架解决了此类问题。我建议查看他们的 link Walkthrough: Creating an Extensible Application。 link 包含从头到尾创建可扩展控制台应用程序的完整演练,并解释了各种场景。您可以从特定文件夹加载加载项。我怀疑它也能解决您可能遇到的问题。
它还处理向后兼容性问题,例如 "New host, old add-ins" 和自定义加载项。
管道场景:新主机,旧加载项。
此管道也在 Add-in Pipeline Scenarios 中进行了描述。
因此,对于 .Net Core,我们有一种不同的方式来创建可扩展的应用程序。以下 Microsoft 文章 Create a .NET Core application with plugins 建议对具有依赖性的插件使用程序集依赖性解析器。
我目前正在开发一个项目,该项目应该作为多个附加组件的框架工作,这些附加组件应该在运行时加载。
我的任务是在我的应用程序文件夹中具有以下结构:
- 2 个包含子文件夹的目录。一个名为“/addons”用于附加组件,另一个名为“/ref”用于这些附加组件可能使用的任何附加引用(如
System.Windows.Interactivity.dll
) - 当从应用程序的菜单中选择一个附加组件时,应该在运行时加载 .dll 并且应该打开一个预设的入口点
- 新加载程序集的所有引用也应加载。
我知道加载项加载时的子文件夹和文件名,所以我只是使用 Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location))
和 Path.Combine()
构建 .dll 的路径,然后通过 [= 加载它14=] 在使用 assembly.GetExportedTypes()
的反射找到为我的 'EntryPointBase' 继承的 class 之前,然后用 Activator.CreateInstance()
.
但是,只要我的附加组件中有任何 引用 ,就会在 assembly.GetExportedTypes()
[= 处弹出针对引用的 System.IO.FileNotFoundException
22=]
我构建了一个方法来加载所有引用的程序集,甚至使其递归加载引用中的所有引用,如下所示:
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 中调试) - 大多数附加组件在 net472 上仍然采用旧的 .csproj 格式
- ref 子文件夹也包含子文件夹(devexpress、system 等),而 addon 子文件夹没有其他子文件夹。
TL;DR;
您正在寻找 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
并在事件处理程序中加载请求的程序集。
无论你有什么样的文件夹结构供参考,你应该做的是:
- 从插件文件夹中获取所有程序集文件
- 从引用文件夹(整个层次结构)获取所有程序集文件
- 处理
AppDomain.CurrentDomain
的AssemblyResolve
并检查请求的程序集名称是否为引用文件夹的可用文件,然后加载和 return 程序集。 - 对于插件文件夹中的每个程序集文件,获取所有类型,如果该类型实现了您的插件接口,则将其实例化并调用其入口点。
例子
在此 PoC 中,我在 运行 时从 Plugins
文件夹中的程序集动态加载 IPlugin
的所有实现,并在加载它们并解析 运行 中的所有依赖项之后-时间,我调用插件的SayHello
方法。
加载插件的应用程序对插件没有任何依赖性,只是在 运行 时从以下文件夹结构加载它们:
这是我加载、解析和调用插件所做的:
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);
}
}
这是结果:
您可以下载或克隆代码:
Microsoft 已经使用其 Add-in 框架解决了此类问题。我建议查看他们的 link Walkthrough: Creating an Extensible Application。 link 包含从头到尾创建可扩展控制台应用程序的完整演练,并解释了各种场景。您可以从特定文件夹加载加载项。我怀疑它也能解决您可能遇到的问题。
它还处理向后兼容性问题,例如 "New host, old add-ins" 和自定义加载项。
管道场景:新主机,旧加载项。
此管道也在 Add-in Pipeline Scenarios 中进行了描述。
因此,对于 .Net Core,我们有一种不同的方式来创建可扩展的应用程序。以下 Microsoft 文章 Create a .NET Core application with plugins 建议对具有依赖性的插件使用程序集依赖性解析器。