WCF 向后兼容性和版本控制策略 – 第三部分






4.88/5 (7投票s)
WCF 框架在不同情况下支持的版本策略。
引言
在本文的第一部分 WCF 向后兼容性和版本策略 – 第一部分 中,我探讨了在不同场景下服务契约(Service Contract)更改的影响。在本文的第二部分 WCF 向后兼容性和版本策略 – 第二部分 中,我探讨了 WCF 框架在数据契约(Data Contracts)版本化不同情况下的行为。在本文中,我将探讨 WCF 框架在不同情况下支持的版本策略。
向后兼容性和版本策略
如果您已阅读前两篇文章,您应该明白 WCF 是版本容错的。版本容错在添加新操作或添加/删除操作参数的情况下是可以接受的。但同时,当在服务中引入破坏性更改时,例如从服务契约中删除操作,或在数据契约中添加/删除必需的成员时,应避免这种情况。考虑到所有这些场景,服务架构师应采用适当的版本策略,以跟上敏捷性、生产力和变更控制的步伐。
以下是在不同场景下采用的版本策略
- 敏捷版本策略
- 半严格版本策略
- 严格版本策略
让我们探讨如何实现每种版本策略
敏捷版本策略
这种版本策略基于 WCF 框架的版本容错行为。这种版本方法尽可能地依赖于向后兼容性,并在兼容性被破坏之前避免正式的契约和终结点版本化。在敏捷版本方法中,我们修改现有的数据契约和服务契约,而不对其进行版本化,也不提供新终结点。下图显示了敏捷版本方法
让我们通过一个示例来理解这种方法。此方法对于需要在生产代码中频繁更新的敏捷环境非常有用。
实现 WCF 服务
让我们开发一个简单的 WCF 服务项目。该 WCF 服务有一个单独的方法 UpdateEmployeeData()
,该方法有一个用户定义类型 EmpInfo
作为参数和返回类型。
EmpInfo
数据契约如下
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
服务实现如下
namespace MyWcfService
{
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName };
}
}
}
构建服务。
版本实践
完成 WCF 服务开发后,构建服务。然后,在 MyService.svc 文件上**右键单击 -> 在浏览器中查看**,以检查 WCF 服务生成的 WSDL。在这里,您可以看到 WCF 框架为服务提供了默认的版本规范。.
合同
默认情况下,服务契约的名称是接口的名称。其默认命名空间是“http://tempuri.org”,每个操作的动作是“http://tempuri.org/contractname/methodname”。建议您显式指定服务契约的名称和命名空间,以及每个操作的动作,以避免使用“http://tempuri.org”并防止接口和方法名称暴露在服务的契约中。现在,让我们跨数据契约和服务契约实现自己的版本规范。为此,我们需要添加如下属性
带版本规范的服务契约
[ServiceContract(Name = "ServiceAContract",
Namespace = "http://www.mynamespace.com/samples/2012/03")]
public interface IMyService
{
[OperationContract]
EmpInfo UpdateEmployeeData(EmpInfo info);
}
带版本规范的数据契约
[DataContract(Name = "EmpInfo",Namespace="http://schemas.mynamespace.com/samples/2012/03")]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
现在,构建服务并再次查看 WSDL,您将看到自己的版本规范
开发一个控制台应用程序以测试服务。
右键单击“**引用”->“添加服务引用”**。单击“**发现”** 或在“**地址”**框中键入服务 URL。单击“**确定”** 后,将生成存根代码。您可以在存根代码的“Reference.cs”文件中看到版本信息。
存根代码摘录
存根代码摘录
注意:当服务端实现了一个新的版本规范,而服务引用没有更新时,消费者客户端将因存根代码上述部分的mismatch而不兼容。
现在使用以下代码在控制台应用程序中调用服务
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WCFTestClient.MyService;
using System.Runtime.Serialization;
namespace WCFTestClient
{
class Program
{
static void Main(string[] args)
{
using (ServiceAContractClient proxy = new ServiceAContractClient())
{
var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik" });
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
}
Console.ReadLine();
}
}
}
运行控制台应用程序。应生成以下输出
服务变更请求
假设上面开发的服务的生产环境已部署,并且已消耗该服务的客户端应用程序运行正常。现在,几天后,其中一个客户端提出请求,要求在其应用程序中更新和显示员工地址。此外,它还需要一项新功能,通过 EmpID 检索数据。为了包含员工地址,需要修改服务端的数据契约。此外,我们还需要在服务端引入一个新方法来通过 ID 获取员工详细信息。同时,我们需要保持与其他客户端的兼容性。在这种情况下,我们可以在 EmpInfo
数据契约中添加一个*非必需*的 DataMembe
r,并添加一个新方法 GetEmployeeDatabyID()
,如下所示
修改后的数据契约
修改后的服务契约
[ServiceContract(Name = "ServiceAContract",
Namespace = "http://www.mynamespace.com/samples/2012/03")]
public interface IMyService
{
[OperationContract]
EmpInfo UpdateEmployeeData(EmpInfo info);
//Adding new method
[OperationContract]
EmpInfo GetEmployeeDatabyID(int EmpID);
}
注意:由于我们遵循敏捷版本方法,因此我们故意避免为服务契约和数据契约引入新的版本规范。
修改后的服务实现
namespace MyWcfService
{
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return info;
}
public EmpInfo GetEmployeeDatabyID(int EmpID)
{
if (EmpID == 1)
return new EmpInfo { EmpID = 1, EmpName = "Kausik", EmpAddress = "Salt Lake" };
else
return null;
}
}
}
构建服务。
现在,在不更新服务引用的情况下运行现有的消费者客户端控制台应用程序。我们希望使用旧的存根代码调用服务。您应该会看到以下输出
因此,我们可以看到,在服务数据契约中添加新的非必需成员后,现有服务的客户端不受影响。
现在,让我们模拟另一个有兴趣显示员工地址的客户端。此客户端应更新服务引用,以便通过 UpdateEmployeeData
方法和新方法 GetEmployeeDatabyID
获取更新员工地址的新功能。
让我们再开发一个控制台应用程序来模拟具有另一个服务消费者的场景。虽然我们在此时创建了应用程序,但可以假定该应用程序是一个现有的客户端,它需要更新员工地址和检索员工详细信息(按 ID)的附加功能。
创建应用程序后,右键单击“**引用”->“添加服务引用”**。单击“**发现”** 或在“**地址”**框中键入服务 URL。单击“**确定”** 后,将生成存根代码。现在,使用以下代码在控制台应用程序中调用服务
运行控制台应用程序。应生成以下输出
因此,我们可以看到,在服务端提供了服务契约和服务数据契约发生更改的新版本后,两个客户端都能无缝运行。一个客户端使用旧功能,另一个客户端使用新功能。尽管现有客户端继续正常运行,但版本容错会带来以下问题
- 在服务契约中添加新操作
- 向数据契约添加非必需成员
- 从数据契约中删除非必需成员
假设一个服务已部署在生产环境中,并被多个客户端使用。现在,如果添加了新的服务方法,服务端的 WSDL 将会更新。现在,并非所有服务使用者都需要更新服务引用才能引用最新的 WSDL。只有需要新功能的那些使用者才需要更新服务引用以在客户端使用它们。因此,除非在进行更改时公开了新终结点,否则无法跟踪哪些现有客户端已更新了其 WSDL。
正如我们在前面的示例中所看到的,如果向数据契约添加了非必需成员,而客户端的存根代码未更新,则将使用默认值或为客户端使用默认值。为了将缺失的值初始化为某些相关值,可能需要编写额外的代码。
在上一篇文章中,我们还讨论过,如果从服务端的数据契约中删除了一个 DataMember
,并且它存在于存根代码中,由于服务未更新,从客户端发送的数据会经过一个往返。由于这个问题,传递给服务或返回给客户端的数据会丢失。
在考虑了所有这些场景之后,才应采用敏捷版本方法。
半严格版本策略
这种方法介于敏捷版本和严格版本之间。在这种版本方法中,会指定正式的契约和终结点版本。当契约或终结点修改时,会跟踪更改。
在半严格方法的一个变体中,新操作被添加到新的 V2 服务契约中,该契约继承了 V1 服务契约。版本化是通过为 V2 服务契约公开新终结点来实现的。现有操作的命名空间保持为原始契约的命名空间,这意味着 V1 客户端可以在不影响现有代码的情况下移动到新终结点,并根据需要更新其代理以反映新操作。图 2 说明了这种半严格版本方法的一种变体
让我们通过一个示例来理解这种方法。
实现 WCF 服务
让我们再开发一个简单的 WCF 服务项目。该 WCF 服务具有与先前示例相同的服务契约和数据契约。我们还在服务中提供了版本规范。
带版本规范的服务契约
[ServiceContract(Name = "ServiceAContract",
Namespace = "http://www.mynamespace.com/samples/2012/03")]
public interface IMyService
{
[OperationContract]
EmpInfo UpdateEmployeeData(EmpInfo info);
}
带版本规范的数据契约
[DataContract(Name = "EmpInfo",
Namespace="http://schemas.mynamespace.com/samples/2012/03")]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
服务实现如下
namespace MyWcfService
{
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return info;
}
}
}
此外,修改 web.config 文件以公开终结点
构建服务。
现在,在另一个控制台应用程序(代码示例中的 WCFClient_Semi_Strict_Ver1)中调用服务。服务应被调用并正常运行。
现在,假设,如前一个场景,几天后,我们需要发布另一个版本的服务,其中包含新方法 GetEmployeeDatabyID
。根据半严格版本策略,我们需要指定正式的契约和终结点版本以跟踪更改。让我们引入一个继承自现有接口的新接口
带有新版本规范的新服务契约
注意:对于契约的两个版本,ServiceContractAttribute
的 Name
属性相同,而接口类型名称从 IMyService
变为 IMyService_Ver2
。
修改服务实现。我们可以从新接口 IMyService_Ver2
实现服务,因为它已从现有接口 IMyService
派生。
更改 web.config 以公开新终结点 IMyService_Ver2
现在,通过服务端此更改,现有客户端可以保持兼容,因为服务契约 IMyService
包含旧的版本规范。对新功能 GetEmployeeDatabyID
感兴趣的客户端可以升级其服务引用以使用新终结点。操作 UpdateEmployeeData
的命名空间保持为原始契约的命名空间,这意味着 V1 客户端可以在不影响现有代码的情况下移动到新终结点,并根据需要更新其代理以反映新操作。
让我们再开发一个控制台应用程序(代码示例中的 WCFClient_Semi_Strict_Ver2)来模拟具有另一个服务消费者的场景。虽然我们在此时创建了应用程序,但可以假定该应用程序是一个现有的客户端,它需要附加功能。请参考控制台应用程序中的修改后的服务。现在,使用以下代码在应用程序中调用现有服务方法和新服务方法
using (ServiceAContractClient proxy = new ServiceAContractClient())
{
var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik" });
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
//Implementing new functionalities in the client application
sc = proxy.GetEmployeeDatabyID(1);
Console.WriteLine("Retrieving Employee Details:");
Console.WriteLine("-------------------------");
Console.WriteLine("Employee ID: " + sc.EmpID);
}
Console.ReadLine();
运行控制台应用程序。它应该显示以下输出
现在,在不更新服务引用的情况下运行另一个现有客户端(代码示例中的 WCFClient_Semi_Strict_Ver1)。现有应用程序应正常运行,因为它与服务保持兼容。
半严格版本化的另一种变体 - 公开单个终结点
在半严格方法的一种变体中,新操作被添加到新的 V2 服务契约中,该契约继承了 V1 服务契约。由于 V2 服务契约继承了 V1 契约,因此保留了原始命名空间用于现有操作。新命名空间仅用于新操作。因此,即使原始终结点已更新以公开 V2 契约,从 V1 客户端发送的消息仍与原始命名空间兼容。只有 V2 客户端将使用新命名空间将消息发送到新操作。在上面的实现中,我们公开了一个新终结点,同时添加了新服务契约。按照这种变体,我们可以在同一个终结点版本化服务契约(继承)。图 3 说明了另一种半严格版本变体
要测试此方法,请修改 web.config 以公开一个具有新契约 IMyService_Ver2
的单个终结点,该契约继承了现有契约 IMyService
。
现在运行服务的两个消费者。您可以看到它们正常运行,不受服务端更改的影响。
严格版本策略
在严格版本中,对于服务契约的所有更改都需要正式的版本规范。在某些情况下,正式版本是必须的。例如,如果您删除服务操作。通过推迟正式版本化,您可以节省不重要更改的开发工作,但会失去跟踪已公开服务版本和跟踪客户端迁移到更新服务功能的能力。在这种版本类型中,每当删除或添加服务方法时,都会公开一个新终结点。即使现有操作未更改,使用严格版本,V2 契约也必须在新命名空间下包含所有现有操作。图 4 说明了另一种半严格版本变体
让我们通过一个示例来理解这种方法。
实现 WCF 服务
让我们再开发一个简单的 WCF 服务项目。该 WCF 服务具有与先前示例相同的服务契约和数据契约。我们还在服务中提供了版本规范。
带版本规范的服务契约
[ServiceContract(Name = "ServiceAContract", Namespace = "http://www.mynamespace.com/samples/2012/03")]
public interface IMyService
{
[OperationContract]
EmpInfo UpdateEmployeeData(EmpInfo info);
}
带版本规范的数据契约
[DataContract(Name = "EmpInfo",Namespace="http://schemas.mynamespace.com/samples/2012/03")]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
服务实现如下
namespace MyWcfService
{
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return info;
}
}
}
此外,修改 web.config 文件以公开终结点
构建服务。
现在,在另一个控制台应用程序(代码示例中的 WCFClient__Strict_Ver1)中调用服务。服务应被调用并正常运行。
现在,假设,如前一个场景,几天后,我们需要发布另一个版本的服务,其中包含新方法 GetEmployeeDatabyID
。根据严格版本策略,我们需要指定正式的契约和终结点版本以跟踪更改。让我们引入一个新接口,其中包含现有的服务方法以及一个新方法
带有新版本规范的新服务契约
注意:对于契约的两个版本,ServiceContractAttribute
的 Name
属性相同,而接口类型名称从 IMyService
变为 IMyService_Ver2
。
修改服务实现。我们可以从新接口 IMyService_Ver2
实现服务。
public class MyService_Strict : IMyService, IMyService_Ver2
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return info;
}
public EmpInfo GetEmployeeDatabyID(int EmpID)
{
if (EmpID == 1)
return new EmpInfo { EmpID = 1, EmpName = "Kausik" };
else
return null;
}
}
更改 web.config 以公开新终结点 IMyService_Ver2
。
让我们再开发一个控制台应用程序(代码示例中的 WCFClient_Strict_Ver2)来模拟具有另一个服务消费者的场景。虽然我们在此时创建了应用程序,但可以假定该应用程序是一个现有的客户端,它需要附加功能。请参考控制台应用程序中的修改后的服务。现在,使用以下代码在应用程序中调用现有服务方法和新服务方法
static void Main(string[] args)
{
using (ServiceAContractClient proxy = new ServiceAContractClient())
{
var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik" });
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
}
using (ServiceAContract1Client proxy = new ServiceAContract1Client())
{
//Implementing new functionalities in the client application
var mc = proxy.GetEmployeeDatabyID(1);
Console.WriteLine("Retrieving Employee Details:");
Console.WriteLine("-------------------------");
Console.WriteLine("Employee ID: " + mc.EmpID);
}
Console.ReadLine();
}
运行控制台应用程序。服务方法应该被调用,应用程序应该正常运行。此外,请检查另一个现有客户端(代码示例中的 WCFClient__Strict_Ver1)。该客户端应该不受影响并且正常运行。
结论
我探讨了不同的版本方法。请根据您的需求尝试使用版本策略。下表根据场景建议了几种版本策略
版本策略 | 场景 |
敏捷版本策略 | 适用于高度依赖向后兼容性且必须容忍频繁更改的环境。 |
严格版本策略 | 适用于更新不频繁或需要严格变更控制的环境。 |
半严格版本策略 | 适用于允许自定义版本策略以满足应用程序需求的坏境。 |