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






4.82/5 (12投票s)
WCF 在不同数据契约版本化情况下的行为。
引言
本文阐述了 WCF 数据契约版本化的几个方面以及为契约提供后向兼容性所采用的实践。一旦服务开发完成并上线到生产环境,契约中的任何更改都应是后向兼容的,这样在部署更改时就不会影响现有客户端。在上一篇文章 WCF 后向兼容性和版本策略 – 第 1 部分 中,我探讨了服务契约在不同场景下更改的影响。在这第二篇文章中,我们将探讨 WCF 在不同数据契约版本化情况下的行为。
跨数据契约的后向兼容性
WCF 服务的修改可能发生在数据契约中。让我们通过一个示例来探讨不同的情况。为了探索 WCF 的后向兼容性行为,我们将开发一个简单的 WCF 服务项目。该 WCF 服务有一个名为 UpdateEmployeeData()
的单一方法,该方法返回一个 EmpInfo
类型,其中包含更新的员工信息。
实现 WCF 服务
让我们来开发一个简单的 WCF 服务项目。该 WCF 服务有一个名为 UpdateEmployeeData()
的单一方法,该方法有一个参数和一个用户定义类型 EmpInfo
的返回类型。
EmpInfo
数据契约为:
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
服务实现如下
namespace MyWcfService
{
// NOTE: You can use the "Rename" command on the "Refactor" menu
// to change the class name "Service1" in code, svc and config file together.
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName };
}
}
}
完成 WCF 服务开发后,右键单击 MyService.svc 文件并选择 **“在浏览器中查看”**,以检查 WCF 服务生成的 WSDL。
开发一个控制台应用程序以测试服务。
右键单击 **“引用”->“添加服务引用”**。单击 **“发现”** 或在 **“地址”** 框中键入服务 URL。单击 **“确定”** 后,将生成存根代码。现在,使用以下代码在控制台应用程序中调用服务:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WCFTestClient.MyService;
namespace WCFTestClient
{
class Program
{
static void Main(string[] args)
{
var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik"});
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
}
}
}
运行控制台应用程序。应生成以下输出
修改数据契约
现在,让我们检查客户端在修改数据契约情况下的行为。
情况 1:添加新的非必需成员
对于数据成员,有一个 IsRequired
属性,其默认值为 false。如果为任何数据成员将该属性设置为 true
,WCF 会告知序列化引擎该值必须出现在底层 XML 中。由于我们没有提及该属性,因此考虑了默认值 false
。
修改 EmpInfo
数据契约。添加一个非必需成员 EmpAddress
。
修改后的数据契约
修改后的服务实现
我们为新添加的成员设置了值。
namespace MyWcfService
{
// NOTE: You can use the "Rename" command on the "Refactor" menu
// to change the class name "Service1" in code, svc and config file together.
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = info.EmpID,
EmpName = info.EmpName, EmpAddress = info.EmpAddress };
}
}
构建服务。
现在,在不更新服务引用的情况下运行现有的消费者客户端控制台应用程序。我们希望使用旧的存根代码调用服务。您应该会看到以下输出:
因此,我们可以看到,在服务数据契约中添加新的非必需成员时,现有的服务客户端不受影响。
情况 2:添加新的必需成员
修改 EmpInfo
数据契约。添加一个必需成员 EmpCity
。
修改后的服务实现
我们为新添加的成员设置了值。
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = 1, EmpName = "Kausik",
EmpAddress = info.EmpAddress, EmpCity = info.EmpCity };
}
}
构建服务。
现在,在不更新服务引用的情况下运行现有的消费者客户端控制台应用程序。预期会发生如下异常:
由于我们添加了一个 required
(必需)的数据成员 EmpCity
,WCF 期望在消息从客户端到服务传输时 EmpCity
的值必须存在。由于我们没有从客户端提供 required
成员 EmpCity
的值,因此应用程序会遇到错误。
情况 3:删除非必需成员
更新服务引用并运行客户端。客户端应成功运行。我们的存根代码也已更新,并且新添加的必需/非必需成员已包含在存根代码的 EmpInfo
类中。修改客户端代码以发送更新成员 EmpAddress
和 EmpCity
的值。
namespace WCFTestClient
{
class Program
{
static void Main(string[] args)
{
using (MyServiceClient proxy = new MyServiceClient())
{
var sc = proxy. UpdateEmployeeData(
new EmpInfo { EmpID = 1, EmpName = "Kausik",
EmpAddress="Salt Lake", EmpCity="Kolkata"});
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
Console.WriteLine("Employee Name: " + sc.EmpAddress);
Console.WriteLine("Employee Name: " + sc.EmpCity);
}
Console.ReadLine();
}
}
}
运行客户端。您应该会看到以下输出:
现在,修改服务以删除 non-required
(非必需)成员 EmpAddress
。
修改后的数据契约
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
[DataMember(IsRequired = true)]
public string EmpCity;
}
修改后的服务实现
namespace MyWcfService
{
// NOTE: You can use the "Rename" command on the "Refactor"
// menu to change the class name "Service1" in code, svc and config file together.
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = info.EmpID,
EmpName = info.EmpName, EmpCity = info.EmpCity };
}
}
}
构建服务。
运行客户端应用程序。您应该会看到以下输出:
在这种情况下,服务无法将完整数据集返回给客户端。为成员 EmpAddress
发送的值在服务端丢失。
情况 4:删除必需成员
修改服务以删除 required
(必需)成员 EmpCity
。
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
修改后的服务实现
namespace MyWcfService
{
// NOTE: You can use the "Rename" command on the "Refactor" menu
// to change the class name "Service1" in code, svc and config file together.
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName};
}
}
}
构建服务。
现在,在不更新服务引用的情况下运行现有的消费者客户端控制台应用程序。预期会发生如下异常:
因此,在这种情况下,当客户端收到来自服务且缺少值的响应时,会抛出异常。
情况 5:修改现有成员数据类型
在修改数据契约时,可能会出现单个数据成员的类型被修改的情况。在探讨这种情况之前,让我们先使应用程序稳定下来。
更新服务引用,并从客户端代码中删除 EmpAddress
/EmpCity
操作部分,如下所示:
namespace WCFTestClient
{
class Program
{
static void Main(string[] args)
{
using (MyServiceClient proxy = new MyServiceClient())
{
var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik"});
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
}
Console.ReadLine();
}
}
}
运行客户端。客户端应成功运行。
为了探讨这种情况,让我们修改 EmpInfo
数据契约的 EmpID
数据成员的类型,如下所示:
构建服务。
现在运行客户端。由于我们修改了原始类型(从 int
改为 string
),客户端可能会成功运行并产生预期的输出。客户端发送的 EmpID 值是 int
,然后将在服务端转换为 string
。但在大多数情况下,如果类型兼容,则不会抛出异常,但可能会收到意外的结果。
版本化往返
让我们考虑 **情况 1** 的场景,并尝试调查新添加的数据成员 EmpAddress
。已知新添加的数据成员已被序列化并通过网络传输。只有我们旧存根代码中的 EmpInfo
类尚未修改以包含新成员 EmpAddress
。因此,在这种情况下,“versionNew
”服务将带有新添加成员的数据发送给“versionOld
”客户端。客户端在处理消息时会忽略新添加的成员,但它会将相同的数据(包括新添加的成员)重新发送回 versionNew
服务。这称为版本化往返。典型场景是数据更新,其中从服务检索数据,进行更改,然后返回。当 WCF 框架遇到不属于原始数据契约的数据时,该数据将被存储在属性中并得以保留。除了临时存储之外,它不会以任何其他方式进行处理。如果对象被返回到其原始位置,则原始(未知)数据也会被返回。因此,数据在没有丢失的情况下,与原始终结点进行了往返。
要为特定类型启用往返,该类型必须实现 IExtensibleDataObject
接口。该接口包含一个属性 ExtensionData
,它返回 ExtensionDataObject
类型。该属性用于存储当前版本未知的数据契约未来版本中的任何数据。
svcutil 工具生成的存根代码也实现了来自 IExtensibleDataObject
接口的数据契约类。在 Visual Studio IDE 中,浏览服务引用并打开 Reference.cs 文件中的存根代码。您可以看到数据契约类 EmpInfo
实现 IExtensibleDataObject
。
ExtensionDataObject
类型不包含公共方法或属性。因此,无法直接访问存储在 ExtensionData
属性中的数据。尽管可以在调试器中查看它。
为了防止服务丢失数据(**情况 3**),我们也可以在服务中实现 IExtensibleDataObject
。为此,我们可以从 IExtensibleDataObject
接口派生数据契约。
[DataContract]
public class EmpInfo: IExtensibleData
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
#region IExtensibleDataObject Members
private ExtensionDataObject extensionData;
public ExtensionDataObject ExtensionData
{
get { return extensionData; }
set { extensionData = value; }
}
#endregion
}
这样做,我们就可以临时保留服务端的数据,同时由于数据契约版本不匹配,客户端发送了额外的数据成员。
修改客户端代码以发送未知数据成员。EmpInfo
存根代码应具有一个额外的数据成员 EmpAddress
,该成员对服务器是未知的。
namespace WCFTestClient
{
class Program
{
static void Main(string[] args)
{
using (MyServiceClient proxy = new MyServiceClient())
{
var sc = proxy.UpdateEmployeeData(
new EmpInfo { EmpID = 1, EmpName = "Kausik", EmpAddress="Salt Lake"});
Console.WriteLine("Employee ID: " + sc.EmpID);
Console.WriteLine("Employee Name: " + sc.EmpName);
Console.WriteLine ("Employee Address: " + sc.EmpAddress);
}
Console.ReadLine();
}
}
}
现在调试服务,您可以在调试器中看到额外的成员。
运行客户端后,您可以看到,尽管服务保留了未知的数据成员 EmpAddress
,但客户端上“Employee Address”的值显示为空白。
修改服务实现如下:
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
//return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName};
return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName, ExtensionData=info.ExtensionData };
}
}
现在运行客户端,您可以看到“EmpAddress
”的值在从服务进行版本往返后已更改。
此外,IExtensibleDataObject
的行为还可以保护服务免受潜在的 DoS 攻击。这可以通过将 ServiceBehaviorAttribute
的 IgnoreExtensionDataObject
属性设置为 true
来实现,如下所示:
[ServiceBehavior(IgnoreExtensionDataObject = true)]
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName};
}
}
此行为也可以在 web.config 中配置,如下所示:
现在,调试服务,您将看到 ExtensionData
中的额外成员在调试器中显示为 null
。
建议所有类型都实现此接口以适应未来新增的未知成员。这样,WCF 数据契约系统就可以随着时间的推移以非破坏性的方式演进,并提供数据契约的前向兼容性。
我尝试讨论了一些数据契约中操作版本化的可能情况。在文章的下一部分也是最后一部分,我将探讨 WCF 框架在不同情况下支持的版本策略。