我正在考虑通过提供一些预定义的接口来向现有应用程序添加一些可扩展性,这些接口可以通过放置在特定位置并由应用程序拾取的“插件”来实现。应用程序的核心很少更新,而插件更新和部署则更加频繁。
基本上,有这样的设置:
// 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。
这是合理且预期的行为,但它给了我这个项目设置方式的一些痛点,即插件的开发/更新不能仅仅通过再次运行构建并放入新插件(现在有较新版本的依赖项)。
(我知道将较新的程序集解析为较旧的程序集的潜在问题以及类型定义中潜在的不匹配。问题只是解决高级程序集解析问题,而不是解决与类型定义不匹配相关的问题。)
我看到了一些让事情正常运转的选择,但没有一个像我真正想要的那么简单:
- 将绑定重定向添加到 web.config,指示所有 core.dll v1+ 引用解析为 core.dll v1 引用
- 创建包含接口定义的“contracts.dll”程序集,并在构建过程中保持该特定程序集的版本号不变
- 针对 core.dll 的部署版本构建插件(以某种方式引用开发中的部署版本)
如前所述,这些对我来说都不是真正容易实现的目标,我希望有人有更好的解决方案?
我在这个领域进行了相当广泛的工作,并且始终发现您需要围绕范围内和范围外的内容划定一些界限。想要最大程度的可扩展性是有风险的,但这一切都是有代价的。该成本要么是编译时(以约定、最佳实践和可能是自动构建检查的形式),要么是复杂的运行时。
要解决您列出的选项:
Binding redirects are only a partial tool to achieve a solution. They will allow your program to 'slip' one version of a DLL in place of another, however, it will not magically solve the problem of what happens when methods change. MissingMethodException?
Something here that you may not have though of aswell, is the dependency chain.
You can see that while the application is treating dependency 'A' as a version 1 object, internally it is creating something from a later version which is being passed back to the application and cast to v1.0 - causing an exception.
This can be tricky to handle - and is just one of the risks.
跨构建保持合约程序集版本相同
这可以优雅地工作,但是,它只是将复杂性从构建时推迟到运行时。
您需要努力确保您的更改不会破坏跨版本的兼容性。更不用说,随着您的应用程序老化,您将在这些合同中收集大量您想要弃用的声明。最终这个文件将变得庞大、繁琐并且让开发人员感到困惑——这甚至还没有考虑到您拥有的所有实体合约!
我不太清楚你的意思是什么,以及它如何涵盖你的问题空间。
您可以采取的另一种方法(我们就是这样做的)是为“SDK”的每个主要版本创建一个新合同。这具有一些政治优势,因为它在一段时间内修复了主要功能,这意味着我们可以将功能请求保持在合理的预期水平,超出此范围的任何内容(需要新一代合同)都将推迟到“下一个主要版本” '。
然而,这确实需要在设计功能时勤奋——在编写合同时要深思熟虑,这样你就可以抢占最明显的需求——但我真的觉得这无论如何都是不言而喻的……它们被称为“合同”因为某种原因。
每个新合同都将存在于版本命名空间(Company.Product.Contracts.v1_0、Company.Product.Contracts.v1_1)中。
我不会链接我的合同(每个新版本的合同都会继承上一个版本)。这样做会让您回到保持版本号相同的问题,在不完全破坏链条的情况下,您永远无法完全摆脱功能。
当您的插件加载时,它可以询问主机它支持什么级别的功能(合同版本) - 以及它是否是旧主机/新插件场景:或者,对插件进行编程以减少其运行时功能以处理较小的主机功能,或者干脆拒绝加载。
无论如何,您可能应该执行这些检查,因为没有什么魔法可以让您的插件利用主机中根本不存在的功能! Microsoft 的 MAF 框架尝试通过使用 shim 基础架构来实现这一目标,但它给大多数人带来了巨大的复杂性。
因此,您需要考虑的一些事情是:
- 确定您的可扩展性需求范围!您想要完成的一切都会花费您持续的维护费用。
- 考虑一下您将如何弃用功能
- 不要忘记您的合约将包含实体和逻辑合约,这些与逻辑合约的考虑因素略有不同,因为它们通常传递更多。
- 仔细考虑每个兼容性检查是否在编译时而不是运行时执行得更好(反之亦然)
- 版本编号!程序集版本号对于运行时行为非常有用,文件版本号可以帮助您在版本控制中跟踪 DLL 直至源代码 - 充分利用它们!
- 如果您正在进行自定义 DLL 解析,请使用 app.config 来定义自定义 DLL 位置,而不是程序集解析事件。配置方法更加可预测,而且,嘿,它是在易于阅读的 Xml 中声明的! Fusion Log Viewer 还将很好地报告 DLL 在探测链中插入的位置,而程序集解析事件将隐藏代码中的所有逻辑和规则。
唯一真正的缺点是,使用 app.config 意味着更改不会生效,直到您的应用程序重新读取配置文件(通常是应用程序重新启动),但如果您正在对插件进行 AppDomain 隔离,您甚至可以解决这个问题。
关于您的“core.dll”问题...
我想,对于你来说,这是一个比较简单的问题。核心合约 DLL 中的所有内容都应该存在于版本命名空间下(参见上文,Company.Product.v1_0 等),因此您的 DLL 也包含版本号实际上是有意义的。这将消除部署到 bin 文件夹时 DLL 相互覆盖的问题。
不要依赖 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 的依赖关系,并提醒用户该插件不兼容。
这个话题有很多内容,如果我想到任何与你的事业相关的其他内容,我会回来查看......
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)