将程序集作为模块/插件加载,同时避免重复和脆弱性

Posted

技术标签:

【中文标题】将程序集作为模块/插件加载,同时避免重复和脆弱性【英文标题】:Loading assemblies as modules/plugins while avoiding duplication and fragility 【发布时间】:2013-10-21 14:53:51 【问题描述】:

我们有一个相当大的 C# 代码库,该产品已被分成许多程序集,以避免单一的产品并强制执行一些代码质量标准(客户特定的功能进入客户特定的程序集中以保持“核心”通用且不受对客户特定业务逻辑的依赖的影响)。我们在内部调用这些插件,但它们更多的是构成整个产品的模块。

其工作方式是将这些模块的 DLL 复制到一个目录中,然后应用程序运行时(ServiceStack IIS Web 应用程序或基于 Quartz 的控制台应用程序)为每个不存在的模块执行 Assembly.LoadFile在已加载的当前程序集列表中 (AppDomain.CurrentDomain.GetAssemblies())。

这个PluginLoader 只加载存在于plugins.config 文件中的程序集,但我认为这与手头的问题几乎无关。

PluginLoader 类的完整代码:

https://gist.github.com/JulianRooze/9f6d1b5e61c855579203

这....有效。有点。虽然它很脆弱,并且存在一个问题,即程序集以这种方式从不同的位置加载两次(通常来自应用程序的 /bin/ 文件夹插件目录)。这似乎是因为在调用PluginLoader 类时,AppDomain.CurrentDomain.GetAssemblies()(在启动时)不一定返回程序将自行加载的程序集的最终列表。因此,如果 /bin/ 中有一个名为 dapper.dll 的程序集(核心和许多插件/模块的公共依赖项)尚未被程序使用,那么它还没有被加载(换句话说:它会懒惰地加载它们)。然后,如果该 dapper.dll 也是由插件提供的,PluginLoader 将看到它尚未加载并将加载它。然后,当程序使用它的 Dapper 依赖项时,它会从 /bin/ 加载 dapper.dll,我们现在已经加载了两个 dapper.dll。

在大多数情况下,这似乎没问题。但是,我们使用 RazorEngine 库,当您尝试编译模板时,该库会抱怨具有相同名称的重复程序集。

在调查这个问题时,我遇到了这个问题:

Is there a way to force all referenced assemblies to be loaded into the app domain?

我尝试了接受的答案和 Jon Skeet 的解决方案。接受的答案有效(尽管我还没有验证是否有任何奇怪的行为)但感觉很讨厌。一方面,这也使程序尝试加载恰好位于 /bin/ 中的本机 DLL,这显然会失败,因为它们不是 .NET 程序集。所以你现在必须尝试-捕捉-吞下它。如果 /bin/ 包含一些实际上不再使用但现在被加载的旧 DLL,我也担心奇怪的副作用。这在生产中不是问题,但它正在开发中(事实上,整个问题在开发中比在生产中更成问题,但在生产中也将赞赏解决此问题的额外稳健性)。

如前所述,我也尝试了 Jon Skeet 的答案,我的实现在方法 LoadReferencedAssemblies 中的 PluginLoader 类的 Gist 中可见。这有两个问题:

    它在某些程序集名称上失败,例如 System.Runtime.Serialization 找不到文件。 它会在稍后导致插件突然无法找到依赖项时出现故障。我还找不到原因。

我还使用托管可扩展性框架进行了简要调查,但我不确定它是否适用。这似乎更旨在为加载组件和定义它们如何交互提供一个框架,而我实际上只对动态加载程序集感兴趣。

那么,鉴于要求“我想从目录中动态加载指定的 DLL 列表,而没有任何机会加载重复的程序集”,最好的解决方案是什么? :)

如果需要的话,我愿意彻底检查插件系统的工作方式。

【问题讨论】:

请问您为什么需要在不同文件夹中复制多个 dll 副本? 您是否评估过为此使用 MEF 或 Castle Windsor 的可能性? @VladL - 我不需要它,它只是发生了。如果插件依赖于 libray X.dll,它将在插件目录中,如果 X.dll 也是程序本身的依赖项,它也将在 /bin/ 中。 @galenus - 我在我的问题中提到了 MEF,但我认为 MEF 并不适合我,因为它似乎针对的是类/接口级别以及它们如何交互,而我只对加载一堆 DLL 感兴趣,仅此而已。我没有要导出的类型或接口,没有组件,也没有想要扩展的东西。这只是程序集。 @JulianR 您不需要将 dll 复制到输出文件夹中。您可以在 app.config 文件中定义它们的路径 【参考方案1】:

有几种方法可以解决这个问题(模块/插件部署在目录层次结构中),坦率地说,您选择了最困难的方法。

最简单的一种是将所有文件夹添加到您的app/web.config 的私有探测路径中。然后将所有对Assembly.LoadFile 的调用替换为Assembly.Load。这将使 .NET 程序集解析机制自动为您解析所有程序集。您不需要加载引用的程序集,因为它们会在需要时自动加载。只有模块/插件必须使用Assembly.Load 加载。

这种方法的缺点是当以下任一情况为真时:

部署的程序集驻留在应用程序库下的目录中。如果您只设置AssemblyName.Codebase 属性,则可以克服这一问题(需要付出代价,请参阅Assembly.Load(AssemblyName) 文档中的注释)。这将使 Load 像 LoadFrom 一样工作。 MEF 在AssemblyCatalog 中使用了这种方法。 您有具有相同标识的不同程序集(不仅仅是文件)。如果是这种情况,那么您可能需要使用不同的程序集命名方法。以 DevExpress 方法为例,该方法具有强名称程序集,文件名中包含程序集版本。这将允许您拥有一个平面目录结构。如果您不能采用这种方法,则对您的程序集进行强命名并将它们部署在 GAC 或不同的文件夹中。如果您无法做到这一点,那么您的所有程序集都将使用您当前的方法加载尽可能少的程序集,但请尝试使用新的强名称版本慢慢替换它们的计划。

请注意,我不熟悉 ServiceStack。在 ASP.NET 中,您可以使用 Assembly.Codebase 属性到 load assemblies deployed outside of bin。

最后看看 Suzanne Cook 在LoadFile vs. LoadFrom 上的博客文章。

【讨论】:

感谢您的回答。我将看看使用Load 而不是LoadFile。插件/模块不在应用程序目录中,而是在共享目录中,因为它们被多个应用程序共享。 我尝试了Assembly.Load 方法。它似乎工作正常,但我仍然不确定CodeBase 部分的来源。我根本没有触及它,但它仍然可以从随机目录加载程序集。 我不确定的另一部分是卷影复制。在我们复制每个要加载到临时目录以避免文件锁定的程序集之前。但是对于Assembly.Load,我们真的不知道它是否会真正加载它或使用它之前已经加载的程序集,所以如果它已经加载,我们将不必要地将它复制到 temp.xml 中。也许没什么大不了的,但它会增加启动时间并污染温度。

以上是关于将程序集作为模块/插件加载,同时避免重复和脆弱性的主要内容,如果未能解决你的问题,请参考以下文章

加载插件时,提示反射异常,调用的目标出现异常,该模块应包含一个程序集清单

如何避免将大文件重复加载到 python 脚本中?

解决程序集,模糊方式

是否可以避免将一些程序集加载到“AppDomain”中?

加载依赖于另一个域的程序集时的 FileNotFound [重复]

细说 webpack 之流程篇