客户端兼容性检查管理
本文关键字:管理 检查 兼容性 客户端 | 更新日期: 2023-09-27 18:30:03
我正在开发一个必须支持向后兼容性的客户端-服务器应用程序(.NET 4,WCF)。换句话说,旧客户端应该与新服务器兼容,反之亦然。因此,我们的客户端代码中充斥着以下语句:
if (_serverVersion > new Version(2, 1, 3))
{
//show/hide something or call method Foo()...
}
else
{
//show/hide something or call method Foo2()...
}
显然,这在某种程度上变成了一场维护噩梦。幸运的是,我们被允许打破每个小版本的向后兼容性。当我们到了可能破坏兼容性的地步时,我想清理上面示例形式的代码。
我的问题:
(1) 当这些代码块不再"有效"时,有没有一种方法可以轻松识别它们?我最初的想法是基于程序集的版本以某种方式有条件地应用Obsolete属性。当我们进入一个新的次要版本时,Obsolete属性会"启动",突然之间,我们会有几个编译器警告指向这些代码块。。。有人做过这样的事吗?或者有更好的方法来处理这个问题吗?
(2) 每当我看到像new Version(2, 1, 3)
这样的硬编码版本时,我都会感到尴尬。更糟糕的是,在开发过程中,我们不知道发布的最终版本,所以当开发人员添加检查时,版本检查基于当前版本号+1。虽然这是有效的,但它不是很干净。有什么想法可以改进吗?
谢谢!
我建议至少创建一个可以执行以下逻辑的方法:
public static class ServerUtilities
{
public static bool IsValidToRun(Version desiredVersion)
{
if (_serverVersion >= desiredVersion)
return true;
else if (/* your other logic to determine if they're in some acceptable range */)
return true;
return false;
}
}
然后,像这样使用:
if (ServerUtilities.IsValidToRun(new Version(2, 1, 3)))
{
// Do new logic
}
else
{
// Do old logic
}
如果您需要集中版本,请有一个静态的功能库到版本映射,这样您就可以调用:
if (ServerUtilities.IsValidToRun(ServerFeatures.FancyFeatureRequiredVersion))
{
...
}
public static class ServerFeatures
{
public static Version FancyFeatureRequiredVersion
{
get { return new Version(2, 1, 3); }
}
}
另一种选择是实现服务契约的版本控制:此时,您可以利用WCF自己的功能来忽略不会破坏客户端的微小更改,如此版本控制策略页面上所列。
在图1中,您可以看到,当向操作签名添加新参数、从操作签名中删除参数和添加新操作时,客户端不受影响。
如果仍有突破性的更改,或者您的客户端必须支持这两个版本(如果我错了,请纠正我,因为我不知道您的部署策略),您可以在不同的端点上提供不同版本的服务,并在客户端代码中有一个WCF客户端工厂,然后可以将其配置为返回相应端点的客户端。
在这一点上,您已经在不同的客户端中隔离了不同的实现,这可能比现在更干净,也不那么像维护噩梦。
非常基本的示例实现来澄清问题:假设我们的服务有两个不同的合同,一个旧合同和一个新合同。
[ServiceContract(Name = "Service", Namespace = "http://stackoverflow.com/2012/03")]
public interface IServiceOld
{
[OperationContract]
void DoWork();
}
[ServiceContract(Name = "Service", Namespace = "http://stackoverflow.com/2012/04")]
public interface IServiceNew
{
[OperationContract]
void DoWork();
[OperationContract]
void DoAdditionalWork();
}
请注意,两个服务的名称相同,但名称空间不同。
让我们来处理这样一个问题,即拥有一个必须能够同时支持扩展服务和新服务以及旧服务的客户端。让我们假设,当我们之前刚刚调用DoWork时,我们想调用DoAdditionalWork方法,并且我们想在客户端处理这种情况,因为假设DoAdditionalWork可能需要客户端提供一些额外的参数。然后服务的配置可以是这样的:
<service name="ConsoleApplication1.Service">
<endpoint address="http://localhost:8732/test/new" binding="wsHttpBinding" contract="ConsoleApplication1.IServiceNew" />
<endpoint address="http://localhost:8732/test/old" binding="wsHttpBinding" contract="ConsoleApplication1.IServiceOld" />
...
</service>
好吧,我们有服务端,现在是有趣的部分:我们希望使用相同的接口与服务进行通信。在这种情况下,我将使用旧的,但您可能需要在两者之间放置一个适配器。理想情况下,在我们的客户端代码中,我们会这样做:
IServiceOld client = *Magic*
client.DoWork();
这种情况下的神奇之处在于一个简单的工厂:
internal class ClientFactory
{
public IServiceOld GetClient()
{
string service = ConfigurationManager.AppSettings["Service"];
if(service == "Old")
return new ClientOld();
else if(service == "New")
return new ClientNew();
throw new NotImplementedException();
}
}
我将使用哪个客户端的决定权委托给了app.config,但您可以在其中插入版本检查。ClientOld的实现只是IServiceOld:的常规WCF客户端
public class ClientOld : IServiceOld
{
private IServiceOld m_Client;
public ClientOld()
{
var factory = new ChannelFactory<IServiceOld>(new WSHttpBinding(), "http://localhost:8732/test/old");
m_Client = factory.CreateChannel();
}
public void DoWork()
{
m_Client.DoWork();
}
...
}
ClientNew实现了我们想要的行为,即调用DoAdditionalWork操作:
public class ClientNew : IServiceOld
{
private IServiceNew m_Client;
public ClientNew()
{
var factory = new ChannelFactory<IServiceNew>(new WSHttpBinding(), "http://localhost:8732/test/new");
m_Client = factory.CreateChannel();
}
public void DoWork()
{
m_Client.DoWork();
m_Client.DoAdditionalWork();
}
...
}
就是这样,现在我们的客户端可以像下面的例子一样使用:
var client = new ClientFactory().GetClient();
client.DoWork();
我们取得了什么成就?使用客户端的代码是从实际WCF客户端必须做的额外工作中抽象出来的,并且将使用哪个客户端的决定委托给工厂。我希望这个样品的一些变化/扩展能满足你的需要。