我正在开发一个必须支持向后兼容性的客户端 - 服务器应用程序(.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。虽然这有效,但它不是很干净。关于如何改进这一点的任何想法?
谢谢!
答案 0 :(得分: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); }
}
}
答案 1 :(得分:0)
另一种方法是实现服务合同的版本控制:此时您可以利用WCF自己的功能来忽略不会破坏客户端的微小更改,如Versioning Strategies页上所列。
在图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客户端必须执行的其他工作以及关于将哪个客户端委派给工厂的决策中抽象出来。我希望这个样本的变化/扩展能够满足您的需求。