4

我正在考虑通过提供一些预定义的接口来为现有应用程序添加一些可扩展性,这些接口可以通过放置在特定位置并由应用程序拾取的“插件”来实现。应用程序的核心很少更新,而插件更新和部署更频繁。

所以基本上,有这样的设置:

// in core assembly (core.dll)
public interface IReportProvider{
     string GenerateReport();
}

// in other assembly(plugin.dll)
public class AwesomeReport : IReportProvider {
     string GenerateReport(){ ... }
}

这两个项目都是同一个构建过程的一部分,核心应用程序会自行部署,插件会在稍后阶段加入。

我的问题是随着时间的推移程序集版本控制和解决。假设已部署 core.dll v1,我想插入一个插件。如果 plugin.dll 引用 core.dll v1,这将非常有用。但是,如果 plugin.dll 是针对更高版本的 core.dll 编译的(作为构建的一部分,比如 v2),则插件无法加载,因为它引用 core.dll v2 但部署的版本只有 core.dll v1。

这是合理且预期的行为,但它给了我在这个项目的设置方式上的几个痛点,即插件的开发/更新不能仅仅通过再次运行构建并放入新插件来完成(现在有较新的版本依赖项)。

(我知道将新程序集解析为旧程序集的潜在问题以及类型定义中的潜在不匹配问题。问题仅在于修复高级程序集解析问题,而不是修复与类型定义不匹配相关的问题。)

我看到了一些让事情正常工作的选择,但没有一个像我真正想要的那么简单:

  1. 向 web.config 添加绑定重定向,指示所有 core.dll v1+ 引用解析为 core.dll v1 引用
  2. 创建一个包含接口定义的“contracts.dll”程序集,并保持此特定程序集的版本号在构建之间保持不变
  3. 针对 core.dll 的部署版本构建插件(以某种方式引用开发中的部署版本)

如前所述,这些对我来说都不是真正容易实现的目标,我希望有人有更好的解决方案?

4

3 回答 3

3

我在这个领域进行了相当广泛的工作,并且一直发现您需要围绕范围内和范围外的确切内容设置一些界限。想要最大的可扩展性是有风险的,但这一切都是有代价的。这个成本要么是编译时,以约定、最佳实践的形式,也许还有自动构建检查——或者它会是一个复杂的运行时。

要解决您列出的选项:

  1. 绑定重定向只是实现解决方案的部分工具。它们将允许您的程序“滑动”一个 DLL 版本来代替另一个版本,但是,它不会神奇地解决方法更改时发生的问题。缺少方法异常?这里你可能没有的东西是依赖链。 依赖链 您可以看到,虽然应用程序将依赖项“A”视为版本 1 对象,但在内部它正在从更高版本创建一些东西,该版本被传递回应用程序并转换为 v1.0 - 导致异常。这可能很难处理——这只是风险之一。

  2. 在构建中保持合约程序集版本相同这可以优雅地工作,但是,它只是将复杂性从构建时推迟到运行时。您需要努力确保您的更改不会破坏跨版本的兼容性。更不用说随着您的应用程序老化,您将在这些合同中收集很多您想要弃用的声明。最终,这个文件将变得庞大、繁琐并且让开发人员感到困惑——这甚至还没有考虑到您拥有的所有实体合约!

  3. 我不太确定您所说的这个是什么意思,以及它如何涵盖您的问题空间。

您可以采取的另一种方法(我们这样做)是为“SDK”的每个主要版本创建一个新合同。这具有一些政治优势,因为它在一段时间内修复了主要功能,这意味着我们可以将功能请求保持在合理的预期水平,并且超出此范围的任何内容(需要新一代合同)都将推迟到“下一个主要版本” '。但是,这确实需要在设计功能时尽职尽责——在编写合同时要深谋远虑,这样你就可以预先考虑最明显的要求——但我真的觉得无论如何这都是不言而喻的......它们被称为“合同”因为某种原因。每个新合同都将存在于版本命名空间(Company.Product.Contracts.v1_0、Company.Product.Contracts.v1_1)中。

我不会链接我的合同(每个新版本的合同都继承最后一个)。这样做会让您回到保持版本号相同的问题,如果不完全破坏链条,您永远无法完全摆脱功能。

当您的插件加载时,它可以询问主机它支持什么级别的功能(合同版本) - 如果它是旧的主机/较新的插件方案:或者,对插件进行编程以减少其运行时功能以处理较少的主机功能,或者干脆拒绝加载。无论如何,您可能应该执行这些检查,因为没有魔法可以让您的插件利用主机中根本不存在的功能!Microsoft 的 MAF 框架试图通过使用 shim 基础结构来实现这一点,但对于大多数人来说,它会导致大量的复杂性。

因此,您需要考虑的一些事项是:

  1. 确定您的可扩展性要求!您想要完成的一切都将花费您进行持续维护。
  2. 想想你将如何弃用功能
  3. 不要忘记您的合约将包含实体以及逻辑合约,这些与逻辑合约的考虑略有不同,因为它们通常被传递得更多。
  4. 仔细考虑是否在编译时而不是运行时更好地执行每个兼容性检查(反之亦然)
  5. 版本编号!程序集版本号非常适合运行时行为,文件版本号可以帮助您在版本控制中将 DLL 跟踪回源 - 充分利用它们!
  6. 如果您正在执行自定义 DLL 解析,请使用 app.config 来定义您的自定义 DLL 位置,而不是程序集解析事件。配置方法更容易预测,而且,嘿,它是在易于阅读的 Xml 中声明的!Fusion Log Viewer 还将很好地报告您的 DLL 在探测链中的插入位置,而 assembly-resolve 事件将隐藏您在代码中的所有逻辑和规则。唯一真正的缺点是使用 app.config 意味着更改不会生效,直到您的应用程序重新读取配置文件(通常是应用程序重新启动),但如果您正在对插件进行 AppDomain 隔离,您甚至可以解决这个问题。

与您的“core.dll”问题有关...我认为,对您来说,这是一个相对简单的问题。您的核心合同 DLL 中的所有内容都应该存在于版本命名空间下(参见上文,Company.Product.v1_0 等),因此您的 DLL 还包含版本号实际上是有意义的。这将消除您的 DLL 在部署到 bin 文件夹时相互覆盖的问题。不要依赖 GAC - 从长远来看,这将是一个痛苦。令人遗憾的是,开发人员似乎总是忘记 GAC 会覆盖所有内容,这可能会成为调试的噩梦——它还会影响您在权限方面的部署方案。

如果您确实需要保持 DLL 名称相同 - 您可以在您的应用程序中创建一个“本地 gac”,这将使您能够以一种不会相互覆盖的方式存储您的 DLL,但仍然可以通过运行时。查看 app.config 中的“绑定重定向”(在此处查看我的答案)。这可以与应用程序 bin 文件夹下的“伪 GAC 文件夹结构”结合使用。然后,您的应用程序将能够找到所需的任何版本的 DLL,而无需任何自定义程序集解析代码逻辑。我将使用您的应用程序部署所有以前支持的 core.dll 版本。如果您的应用程序升级到版本 9,并且您决定同时支持版本 7 和 8 的插件,那么您只需要包含 Core.7.dll、Core.8.dll 和 Core.9.dll。您的插件加载逻辑应该检测对旧版本 Core 的依赖关系,并提醒用户插件不兼容。

这个话题有很多,如果我想到与您的事业相关的其他任何事情,我会回来查看...

于 2012-09-03T01:16:10.510 回答
0

无论您的原因是什么来避免您的 3 点(我认为这更可靠和恰当),您都可以利用Assembly Resolve Event

在 core.dll 中包含如下代码

static Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args)
       {
            //replace the if check with more reliable check such as matching the public key as well
            if (args.Name.Contains(Assembly.GetExecutingAssembly().GetName().Name))
            {
                Console.WriteLine("Resolved " + args.Name + " as " + Assembly.GetExecutingAssembly().GetName());
                return Assembly.GetExecutingAssembly();
            }

            return null;
       }

在尝试加载插件之前绑定上述处理程序。如果您在当前 Appdomain 中加载插件,则将其绑定到AssemblyResolve当前域。

例如

[SecurityPermission(SecurityAction.Demand, ControlAppDomain = true)]
public static void LoadPlugins()
{
    AppDomain.CurrentDomain.AssemblyResolve += AppDomain_AssemblyResolve;
    Assembly pluginAssembly = AppDomain.CurrentDomain.Load("MyPlugin");
}
于 2012-08-29T13:46:27.037 回答
0

您也许可以使用绑定重定向

于 2012-08-29T16:37:41.253 回答