这是我们在客户网站上遇到的一种相当不愉快的泡菜。客户端有大约100个工作站,我们在其上部署了产品“MyApp”的1.0.0版本。
现在,该产品所做的一件事就是它加载一个加载项(称之为“MyPlugIn”,它首先在中央服务器上查找是否有更新的版本,如果它是那么它在本地复制该文件,然后使用Assembly.Load
加载该加载项并调用某个已知的接口。这已经好几个月了。
然后客户想在某些机器上安装我们产品的v1.0.1(但不是全部)。随之而来的是MyPlugIn的新版本。
但后来出现了问题。有一个共享的DLL,由MyApp和MyPlugIn引用,称为MyDLL,它有一个方法MyClass.MyMethod
。在v1.0.0和v1.0.1之间,MyClass.MyMethod
的签名发生了变化(添加了一个参数)。现在新版本的MyPlugIn导致v1.0.0客户端应用程序崩溃:
找不到方法:MyClass.MyMethod(System.String)
客户端明确地不希望在所有客户端站点上部署v1.0.1,因为v1.0.1中包含的修复程序仅对少数工作站是必需的,并且不需要将其推广到所有客户端。遗憾的是,我们还没有(还)使用ClickOnce或其他大规模部署实用程序,因此推出v1.0.1将是一项痛苦而且不必要的练习。
是否有某种方法可以在MyPlugin中编写代码,以便它能够同样正常工作,无论它是处理MyDLL v1.0.0还是v1.0.1?也许在实际调用它之前,有一些方法可以使用反射来探测预期的接口,看它是否存在?
编辑:我还应该提一下 - 我们有一些非常严格的QA程序。由于QA已正式发布v1.0.1,因此我们不允许对MyApp或MyDLL进行任何更改。我们唯一的行动自由是更改MyPlugin,这是专门为该客户编写的自定义代码。
答案 0 :(得分:4)
问题是您所做的更改必须基本,而不是更改。因此,如果您希望在部署中恢复兼容(就像我在当前部署策略中所理解的那样,这是唯一的选择),您应该从不 更改界面但是添加一个新的方法,避免插件与共享DLL的紧密链接,但动态加载它。在这种情况下
你将添加一个新的功能而不会打扰旧功能
您可以选择在运行时加载哪个版本的dll。
答案 1 :(得分:3)
我从我前一段时间写过的应用程序中提取了这段代码并删除了一些部分 这里假设了很多东西:
using System.Reflection;
private void CallPluginMethod(string param)
{
// Is MyDLL.Dll in current directory ???
// Probably it's better to call Assembly.GetExecutingAssembly().Location but....
string libToCheck = Path.Combine(Environment.CurrentDirectory, "MyDLL.dll");
Assembly a = Assembly.LoadFile(libToCheck);
string typeAssembly = "MyDll.MyClass"; // Is this namespace correct ???
Type c = a.GetType(typeAssembly);
// Get all method infos for public non static methods
MethodInfo[] miList = c.GetMethods(BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly);
// Search the one required (could be optimized with Linq?)
foreach(MethodInfo mi in miList)
{
if(mi.Name == "MyMethod")
{
// Create a MyClass object supposing it has an empty constructor
ConstructorInfo clsConstructor = c.GetConstructor(Type.EmptyTypes);
object myClass = clsConstructor.Invoke(new object[]{});
// check how many parameters are required
if(mi.GetParameters().Length == 1)
// call the new interface
mi.Invoke(myClass, new object[]{param});
else
// call the old interface or give out an exception
mi.Invoke(myClass, null);
break;
}
}
}
我们在这里做什么:
MyClass
。MethodInfo
列表。Invoke
调用正确的版本。答案 2 :(得分:3)
我的团队犯了同样的错误。我们有类似的插件架构,从长远来看,我能给你的最好的建议就是尽快改变这种架构。这是一个可维护性的噩梦。向后兼容性矩阵随每个版本非线性增长。严格的代码审查可以提供一些缓解,但问题是您始终需要知道何时添加或更改方法以便以适当的方式调用它们。除非开发人员和审阅者都确切知道上次更改方法的时间,否则在找不到方法时会冒运行时异常的风险。您永远不能安全地在插件中调用MyDLL中的新方法,因为您可能在没有最新MyDLL版本的旧客户端上运行。
目前,您可以在MyPlugin中执行以下操作:
static class MyClassWrapper
{
internal static void MyMethodWrapper(string name)
{
try
{
MyMethodWrapperImpl(name);
}
catch (MissingMethodException)
{
// do whatever you need to to make it work without the method.
// this may go as far as re-implementing my method.
}
}
private static void MyMethodWrapperImpl(string name)
{
MyClass.MyMethod(name);
}
}
如果MyMethod不是静态的,你可以创建一个类似的非静态包装器。
至于长期变化,你可以做的一件事是给你的插件接口进行通信。发布后不能更改接口,但可以定义插件的更高版本将使用的新接口。此外,您无法从MyPlugIn调用MyDLL中的静态方法。如果您可以在服务器级别更改内容(我意识到这可能超出您的控制范围),另一种选择是提供某种版本控制支持,以便新插件可以声明它不适用于旧客户端。然后,旧客户端将仅从服务器下载旧版本,而较新的客户端将下载新版本。
答案 3 :(得分:2)
实际上,更改版本之间的合同听起来不错。在面向对象的环境中,您应该创建一个新契约,可能继承旧契约。
public interface MyServiceV1 { }
public interface MyServiceV2 { }
在内部,您可以使引擎使用新界面,并提供适配器将旧对象转换为新界面。
public class V1ToV2Adapter : MyServiceV2 {
public V1ToV2Adapter( MyServiceV1 ) { ... }
}
加载程序集后,扫描它并执行:
使用黑客(比如测试界面)迟早会咬你或其他任何使用合同的人 - 任何依赖于界面的人都必须知道黑客的细节,从面向对象的角度来看这听起来很糟糕。
答案 4 :(得分:1)
在MyDLL 1.0.1中,弃用旧的MyClass.MyMethod(System.String)
并使用新版本重载它。
答案 5 :(得分:1)
你能否重载MyMethod以接受MyMethod(字符串)(版本1.0.0兼容)和MyMethod(字符串,字符串)(v1.0.1版本)?
答案 6 :(得分:1)
鉴于这种情况,我认为你唯一可以做的就是让两个版本的MyDLL“并排”运行,
这意味着Tigran建议的东西,动态加载MyDLL - 例如作为一个没有相关但可能会帮助你的一个侧面示例,请看一下RedemptionLoader http://www.dimastr.com/redemption/security.htm#redemptionloader(这是一个Outlook插件,它经常会出现问题,导致彼此引用不同版本的助手dll,就像一个背景故事 - 这是涉及COM的一个更复杂的原因,但在这里没有太大变化) -
这是你可以做的,类似的东西。通过它的位置动态加载dll,名称 - 您可以在内部指定该位置,硬编码,甚至可以从配置或其他东西设置它(或者如果您看到MyDll的版本不正确则检查并执行此操作),<登记/>
然后'包装'对象,调用形成动态加载的dll以匹配你通常拥有的 - 或做一些这样的技巧(你必须在实现上包装一些东西或'fork')以使一切都在两种情况下都有效。
还要加上'no-nos'和你的QA悲伤:),
它们不应该破坏从1.0.0到1.0.1的向后兼容性 - 那些(通常)是微小的变化,修复 - 不破坏变化,需要主要版本#。