WCF技术剖析之十六:数据契约的等效性和版本控制

数据契约是对用于交换的数据结构的描述,是数据序列化和反序列化的依据。在一个WCF应用中,客户端和服务端必须通过等效的数据契约方能进行有效的数据交换。随着时间的推移,不可避免地,我们会面临着数据契约版本的变化,比如数据成员的添加和删除、成员名称或者命名空间的修正等,如何避免数据契约这种版本的变化对客户端现有程序造成影响,就是本节着重要讨论的问题。

一、数据契约的等效性

数据契约就是采用一种厂商中立、平台无关的形式(XSD)定义了数据的结构,而WCF通过DataContractAttribute和DataMemberAttribute旨在给相应的类型加上一些元数据,帮助DataContractSerializer将相应类型的对象序列化成具有我们希望结构的XML。在客户端,WCF的服务调用并不完全依赖于某个具体的类型,客户端如果具有与服务端完全相同的数据契约类型定义,固然最好。如果客户端现有的数据契约类型与发布出来数据契约具有一些差异,我们仍然可以通过DataContractAttribute和DataMemberAttribute这两个特性使该数据契约与之等效。

简言之,如果承载相同数据的两个不同数据契约类型对象最终能够序列化出相同的XML,那么这两个数据契约就可以看成是等效的数据契约。等效的数据契约具有相同的契约名称、命名空间和数据成员,同时要求数据成员出现的先后次序一致。比如,下面两种形式的数据契约定义,虽然它们的类型和成员命名不一样,甚至对应成员在各自类型中定义的次序都不一样,但是由于合理使用了DataContractAttribute和DataMemberAttribute这两个特性,确保了它们的对象最终序列化后具有相同的XML结构,所以它们是两个等效的数据契约。

 1: [DataContract(Namespace = "http://www.artech.com/")]
 2: public class Customer
 3: {
 4: [DataMember(Order=1)]
 5: public string FirstName
 6: {get;set;}
 7:  
 8: [DataMember(Order = 2)]
 9: public string LastName
 10: { get; set; }
 11:  
 12: [DataMember(Order = 3)]
 13: public string Gender
 14: { get; set; }
 15: }
 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com/")]
 2: public class Contact
 3: {
 4: [DataMember(Name = "LastName", Order = 2)]
 5: public string Surname
 6: { get; set; }
 7:  
 8: [DataMember(Name = "FirstName", Order = 1)]
 9: public string Name
 10: { get; set; }
 11:  
 12: [DataMember(Name = "Gender", Order = 3)]
 13: public string Sex
 14: { get; set; }
 15: }

数据契约版本的差异最主要的表现形式是数据成员的添加和删除。如何保证在数据契约中添加一个新的数据成员,或者是从数据契约中删除一个现有的数据成员的情况下,还能保证现有客户端的正常服务调用(对于服务提供者),或者对现有服务的正常调用(针对服务消费者),这是数据契约版本控制需要解决的问题。

二、数据成员的添加

先来谈谈添加数据成员的问题,如下面的代码所示,在现有数据契约(CustomerV1)基础上,在服务端添加了一个新的数据成员: Address。但是客户端依然通过数据契约CustomerV1进行服务调用。那么,客户端按照CustomerV1的定义对于Customer对象进行序列化,服务端则按照CustomerV2的定义对接收的XML进行反序列化,会发现缺少Address成员。那么在这种数据成员缺失的情况下,DataContractSerializer又会表现出怎样的序列化与反序列化行为呢?

 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV1
 3: {
 4: [DataMember]
 5: public string Name
 6: { get; set; }
 7:  
 8: [DataMember]
 9: public string PhoneNo
 10: { get; set; }
 11: }
 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV2
 3: {
 4: [DataMember]
 5: public string Name
 6: { get; set; }
 7:  
 8: [DataMember]
 9: public string PhoneNo
 10: { get; set; }
 11:  
 12: [DataMember]
 13: public string Address
 14: { get; set; }
 15: }

为了探求DataContractSerializer在数据成员缺失的情况下如何进行序列化与反序列化,我写了下面一个辅助方法Deserialize<T>用于反序列化工作。

 1: public static T Deserialize<T>(string fileName)
 2: {
 3: DataContractSerializer serializer = new DataContractSerializer(typeof(T));
 4: using (XmlReader reader = new XmlTextReader(fileName))
 5: {
 6: return (T)serializer.ReadObject(reader);
 7: }
 8: }

通过下面的代码来模拟DataContractSerializer在XML缺少了数据成员Address时能否正常的反序列化:先将创建的CustomerV1对象序列化到一个XML文件中,然后读取该文件,按照CustomerV2的定义进行反序列化。从运行的结果可以得知,在数据成员缺失的情况下,反序列化依然可以顺利进行,只是会保留Address属性的默认值。

 1: string fileName = @"e:\customer.xml";
 2: CustomerV1 customerV1 = new CustomerV1
 3: {
 4: Name = "Foo",
 5: PhoneNo = "9999-99999999"
 6: };
 7: Serialize<CustomerV1>(customerV1, fileName);
 8:  
 9: CustomerV2 customerV2 = Deserialize<CustomerV2>(fileName);
 10: Console.WriteLine("customerV2.Name: {0}\ncustomerV2.PhoneNo: {1}\ncustomerV2.Address: {2}",
 11: customerV2.Name ?? "Empty", customerV2.PhoneNo ?? "Empty", customerV2.Address ?? "Empty");

输出结果:

 1: customerV2.Name:Foo
 2: customerV2.Phone:9999-99999999
 3: customerV2.Address: Empty

如果我们从数据契约的另外一种表现形式(XSD)来理解这种序列化和反序列化行为,就会更加容易理解。下面是数据契约CustomerV2通过XSD的表示,从中可以看出对于表示数据成员的每一个XML元素,其minOccurs属性为“0”,就意味着所有的成员都是可以缺省的。由于基于CustomerV1对象序列化后的XML依然符合基于CustomerV2的XSD,所以能够确保反序列化的正常进行。

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <xs:schema elementFormDefault="qualified" targetNamespace="http://www.artech.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.artech.com">
 3: <xs:complexType name="Customer">
 4: <xs:sequence>
 5: <xs:element minOccurs="0" name="Address" nillable="true" type="xs:string"/>
 6: <xs:element minOccurs="0" name="Name" nillable="true" type="xs:string"/>
 7: <xs:element minOccurs="0" name="PhoneNo" nillable="true" type="xs:string"/>
 8: </xs:sequence>
 9: </xs:complexType>
 10: <xs:element name="Customer" nillable="true" type="tns:Customer"/>
 11: </xs:schema>

在很多情况下,要对这些缺失的成员设置一些默认值。我们可以通过注册序列化回调方法的方式来初始化这些值。WCF允许我们通过自定义特性的方式注册序列化的回调方法,这些DataContractSerializer在进行序列化或者反序列化过程中,会回调你注册的回调方法。WCF中定义了4个这样的特性:OnSerializingAttribute,OnSeriallizedAttribute、OnDeserializingAttribute和OnDeserializedAttribute,相应的回调方法分别会在序列化之前、之后,以及反序列化之前、之后调用。

注: 上面4个特性只能用于方法上面,而且方法必须具有这样的签名:void Dosomething(StreamingContext context),即返回类型为void,具有唯一个StreamingContext类型参数。

比如在下面的代码中,通过一个应用了OnDeserializingAttribute特性的方法,为缺失成员Address指定了一个默认值。

 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV2
 3: {
 4: //其他成员
 5: [OnDeserializing]
 6: void OnDeserializing(StreamingContext context)
 7: {
 8: this.Address = "Temp Address...";
 9: }
 10: }

但是对于那些必备数据成员(DataMemberAttribute特性的IsRequired属性为true)缺失的情况,还能够保证正常的序列化与反序列化吗?

 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV2
 3: {
 4: //其他成员
 5: [DataMember(IsRequired =true)]
 6: public string Address
 7: { get; set; }
 8: }

在上面的代码中,我通过DataMemberAttribute的IsRequired属性将Address定义成数据契约的必备数据成员。如果我们运行上面的程序,将会抛出如图1所示SerializationException异常,提示找不到Address元素。

clip_image002

图1 缺少必须数据成员导致反序列化异常

对于上面的异常,仍然可以从XSD找原因。下面是包含必备成员Address的数据契约在XSD中的表示。我们可以清楚地看到Address元素的minOccurs="0"没有了,表明该元素是不能缺失的。由于XML不再符合XSD的定义,反序列化不能成功进行。

 1: <?xml version="1.0" encoding="utf-8"?>
 2: <xs:schema elementFormDefault="qualified" targetNamespace="http://www.artech.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.artech.com">
 3: <xs:complexType name="Customer">
 4: <xs:sequence>
 5: <xs:element name="Address" nillable="true" type="xs:string"/>
 6: <xs:element minOccurs="0" name="Name" nillable="true" type="xs:string"/>
 7: <xs:element minOccurs="0" name="PhoneNo" nillable="true" type="xs:string"/>
 8: </xs:sequence>
 9: </xs:complexType>
 10: <xs:element name="Customer" nillable="true" type="tns:Customer"/>
 11: </xs:schema>

三、数据成员的删除

讨论了数据成员添加的情况,接着讨论数据成员删除的情况。依然沿用Customer数据契约的例子,在这里,两个版本需要做一下转变:CustomerV1中定义了3个数据成员,在CustomerV2 中数据成员Address从成员列表中移除。如果DataContractSerializer按照CustomerV2的定义对CustomerV1的对象进行序列化,那么XML中将不会包含Address成员;同理,如果DataContractSerializer按照CustomerV2的定义反序列化基于CustomerV1的XML,仍然能够正常创建CustomerV2对象,因为CustomerV2的所有成员都存在于XML中。

 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV1
 3: {
 4: [DataMember]
 5: public string Name
 6: { get; set; }
 7:  
 8: [DataMember]
 9: public string PhoneNo
 10: { get; set; }
 11:  
 12: [DataMember]
 13: public string Address
 14: { get; set; }
 15:  
 16: }
 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV2
 3: {
 4: [DataMember]
 5: public string Name
 6: { get; set; }
 7:  
 8: [DataMember]
 9: public string PhoneNo
 10: { get; set; }
 11: }

在这里着重讨论的是由于数据契约成员的移除导致在发送-回传(Round Trip)过程中数据的丢失问题。如图5-9所示,客户端基于数据契约CustomerV1进行服务调用,而服务的实现却是基于CustomerV2的。那么序列化的CustomerV1对象生成的XML通过消息传到服务端,服务端会按照CustomerV2进行反序列化,毫无疑问Address的数据会被丢弃。如果Customer的信息需要返回到客户端,服务需要对CustomerV2对象进行序列化,序列化生成的XML肯定已无Address数据成员存在,当回复消息返回到客户端后,客户端按照CustomerV1进行反序列化生成CustomerV1对象,会发现原本赋了值的Address属性现在变成null了。对于客户端来说,这是一件很奇怪、也是不可接受的事情:“为何数据经过发送-回传后会无缘无故丢失呢?”

clip_image004

图2 消息发送-回传过程中导致数据丢失

为了解决这类问题,WCF定义了一个特殊的接口System.Runtime.Serialization.IExtensibleDataObject,IExtensibleDataObject中仅仅定义了一个ExtensionDataObject类型属性成员。对于实现了IExtensibleDataObject的数据契约,DataContractSerializer在进行序列化时会将ExtensionData属性的值也序列化到XML中;在反序列化过程中,如果发现XML包含有数据契约中没有的数据,会将多余的数据进行反序列化,并将其放入ExtensionData属性中保存起来,由此解决数据丢失的问题。

 1: public interface IExtensibleDataObject
 2: {
 3: ExtensionDataObject ExtensionData { get; set; }
 4: }

比如,让CustomerV2实现IExtensibleDataObject接口。

 1: [DataContract(Name = "Customer", Namespace = "http://www.artech.com")]
 2: public class CustomerV2 : IExtensibleDataObject
 3: {
 4: //其他成员
 5: public ExtensionDataObject ExtensionData
 6: { get; set; }
 7: }

我们通过下面的程序来演示IExtensibleDataObject接口的作用。将CustomerV1对象序列化到第一个XML文件中,然后读取该文件基于CustomerV2进行反序列化创建CustomerV2对象,最后序列化CustomerV2对象到第2个XML文件中。会发现尽管CustomerV2没有定义Address属性,最终序列化出来的XML却包含Address XML元素。

 1: string fileNameV1 = @"e:\customer.v1.xml";
 2: string fileNameV2 = @"e:\customer.v2.xml";
 3: CustomerV1 customerV1 = new CustomerV1
 4: {
 5: Name = "Foo",
 6: PhoneNo = "9999-99999999",
 7: Address="#328, Airport Rd, Industrial Park, Suzhou Jiangsu Proivnce"
 8: };
 9: Serialize<CustomerV1>(customerV1, fileNameV1);
 10: CustomerV2 customerV2 = Deserialize<CustomerV2>(fileNameV1);
 11: Serialize<CustomerV2>(customerV2, fileNameV2);
 1: <Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com/">
 2: <Address>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Proivnce</Address>
 3: <Name>Foo</Name>
 4: <PhoneNo>9999-99999999</PhoneNo>
 5: </Customer>

在介绍DataContractSerializer的时候,知道DataContractSerializer具有只读的属性IgnoreExtensionDataObject(该属性在相应的构造函数中指定),它表示对于实现了IExtensibleDataObject接口的数据契约,在序列化或者反序列化时是否忽略ExtensionData属性的值,该属性默认为false。如果将其设为true,DataContractSerializer在反序列化的时候会忽略多余的XML元素,在序列化时会丢弃ExtensionData属性中保存的值。

 1: public sealed class DataContractSerializer : XmlObjectSerializer
 2: {
 3: //其他成员
 4: public bool IgnoreExtensionDataObject { get; }
 5: }

对于WCF服务,可以通过ServiceBehaviorAttribute的IgnoreExtensionDataObject设置是否忽略ExtensionData。如下面的代码所示。

 1: [ServiceBehavior(IgnoreExtensionDataObject = true)]
 2: public class CustomerManagerService : ICustomerManager
 3: {
 4: public void AddCustomer(CustomerV2 customer)
 5: {
 6: //省略实现
 7: }
 8: }
 9:  

IgnoreExtensionDataObject属性同样可以通过配置的方式进行设定。

 1: <?xml version="1.0" encoding="utf-8" ?>
 2: <configuration>
 3: <system.serviceModel>
 4: <behaviors>
 5: <serviceBehaviors>
 6: <behavior name="IgnoreExtensionDataBehavior">
 7: <dataContractSerializer ignoreExtensionDataObject="true" />
 8: </behavior>
 9: </serviceBehaviors>
 10: </behaviors>
 11: <services>
 12: <service behaviorConfiguration="IgnoreExtensionDataBehavior" name="Artech.DataContractSerializerDemos.CustomerManagerService">
 13: <endpoint address="http://127.0.0.1:9999/customermanagerservice"
 14: binding="basicHttpBinding" contract="Artech.DataContractSerializerDemos.ICustomerManager" />
 15: </service>
 16: </services>
 17: </system.serviceModel>
 18: </configuration>

作者:Artech
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2009-07-21 23:25 Artech 阅读(...) 评论(...) 编辑 收藏