【问题标题】:Client compatibility check management客户端兼容性检查管理
【发布时间】:2012-04-18 04:40:27
【问题描述】:

我正在开发一个必须支持向后兼容性的客户端-服务器应用程序(.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 时开发人员添加检查。虽然这有效,但它不是很干净。关于如何改进的任何想法?

谢谢!

【问题讨论】:

    标签: c# .net wcf c#-4.0 client-server


    【解决方案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); }
        }
    }
    

    【讨论】:

    • 感谢您的快速回复!至少,我认为我们应该使用像 IsValidToRun 这样的方法来集中这个逻辑。我认为 ServerFeatures 类是个好主意,并且可以帮助我们跟踪在推出新版本时可以消除哪些检查。
    • @JohnRussell,这也将为您提供一种故意破坏构建的好方法。当您不再需要检查、编译并修复所有依赖它的损坏位置时,您删除了 ServerFeatures 版本。
    • 没错!我喜欢!我想我实际上会让 ServerFeature 成为枚举,然后在辅助方法中执行版本到 ServerFeature 的映射,例如一个大的 switch 语句。这将允许我们为多个功能重复使用相同的版本,并将所有这些逻辑集中在一个地方。 IsValidToRun 然后可以将 ServerFeature 作为参数,并希望阻止消费者使用动态版本对象。感谢您的帮助!
    【解决方案2】:

    另一种方法是实现您的服务合同的版本控制:此时您可以利用 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 客户端必须做的额外工作中抽象出来的,并且关于使用哪个客户端的决定委托给工厂。我希望这个示例的一些变化/扩展能够满足您的需求。

    【讨论】:

      猜你喜欢
      • 2018-12-31
      • 2020-12-22
      • 2020-10-26
      • 1970-01-01
      • 1970-01-01
      • 2022-08-09
      • 2019-05-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多