閱讀226 返回首頁    go 阿裏雲 go 技術社區[雲棲]


WCF技術剖析之十六:數據契約的等效性和版本控製

數據契約是對用於交換的數據結構的描述,是數據序列化和反序列化的依據。在一個WCF應用中,客戶端和服務端必須通過等效的數據契約方能進行有效的數據交換。隨著時間的推移,不可避免地,我們會麵臨著數據契約版本的變化,比如數據成員的添加和刪除、成員名稱或者命名空間的修正等,如何避免數據契約這種版本的變化對客戶端現有程序造成影響,就是本節著重要討論的問題。

一、數據契約的等效性

數據契約就是采用一種廠商中立、平台無關的形式(XSD)定義了數據的結構,而WCF通過DataContractAttribute和DataMemberAttribute旨在給相應的類型加上一些元數據,幫助DataContractSerializer將相應類型的對象序列化成具有我們希望結構的XML。在客戶端,WCF的服務調用並不完全依賴於某個具體的類型,客戶端如果具有與服務端完全相同的數據契約類型定義,固然最好。如果客戶端現有的數據契約類型與發布出來數據契約具有一些差異,我們仍然可以通過DataContractAttribute和DataMemberAttribute這兩個特性使該數據契約與之等效。

簡言之,如果承載相同數據的兩個不同數據契約類型對象最終能夠序列化出相同的XML,那麼這兩個數據契約就可以看成是等效的數據契約。等效的數據契約具有相同的契約名稱、命名空間和數據成員,同時要求數據成員出現的先後次序一致。比如,下麵兩種形式的數據契約定義,雖然它們的類型和成員命名不一樣,甚至對應成員在各自類型中定義的次序都不一樣,但是由於合理使用了DataContractAttribute和DataMemberAttribute這兩個特性,確保了它們的對象最終序列化後具有相同的XML結構,所以它們是兩個等效的數據契約。

 1: [DataContract(Namespace = "https://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 = "https://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 = "https://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 = "https://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="https://www.artech.com" xmlns:xs="https://www.w3.org/2001/XMLSchema" xmlns:tns="https://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 = "https://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 = "https://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="https://www.artech.com" xmlns:xs="https://www.w3.org/2001/XMLSchema" xmlns:tns="https://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 = "https://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 = "https://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 = "https://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="https://www.w3.org/2001/XMLSchema-instance" xmlns="https://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="https://127.0.0.1:9999/customermanagerservice"
 14: binding="basicHttpBinding" contract="Artech.DataContractSerializerDemos.ICustomerManager" />
 15: </service>
 16: </services>
 17: </system.serviceModel>
 18: </configuration>


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 12:04:16

  上一篇:go  WCF技術剖析之十五:數據契約代理(DataContractSurrogate)在序列化中的作用
  下一篇:go  WCF技術剖析之十七:消息(Message)詳解(上篇)