天天看點

[原創] WCF技術剖析之十六:資料契約的等效性和版本控制

[愛心連結:拯救一個25歲身患急性白血病的女孩[内有蘇州電視台經濟頻道《天天山海經》為此錄制的節目視訊(蘇州話)]]資料契約是對用于交換的資料結構的描述,是資料序列化和反序列化的依據。在一個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元素。

[原創] WCF技術剖析之十六:資料契約的等效性和版本控制

圖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了。對于用戶端來說,這是一件很奇怪、也是不可接受的事情:“為何資料經過發送-回傳後會無緣無故丢失呢?”

[原創] WCF技術剖析之十六:資料契約的等效性和版本控制

圖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>      

注:部分内容節選自《WCF技術剖析(卷1)》第五章:序列化與資料契約(Serialization and Data Contract)

[原創] WCF技術剖析之十六:資料契約的等效性和版本控制

WCF技術剖析系列:

WCF技術剖析之一:通過一個ASP.NET程式模拟WCF基礎架構

WCF技術剖析之二:再談IIS與ASP.NET管道

WCF技術剖析之三:如何進行基于非HTTP的IIS服務寄宿

WCF技術剖析之四:基于IIS的WCF服務寄宿(Hosting)實作揭秘

WCF技術剖析之五:利用ASP.NET相容模式建立支援會話(Session)的WCF服務

WCF技術剖析之六:為什麼在基于ASP.NET應用寄宿(Hosting)下配置的BaseAddress無效

WCF技術剖析之七:如何實作WCF與EnterLib PIAB、Unity之間的內建

WCF技術剖析之八:ClientBase<T>中對ChannelFactory<T>的緩存機制

WCF技術剖析之九:服務代理不能得到及時關閉會有什麼後果?

WCF技術剖析之十:調用WCF服務的用戶端應該如何進行異常處理

WCF技術剖析之十一:異步操作在WCF中的應用(上篇)

WCF技術剖析之十一:異步操作在WCF中的應用(下篇)

WCF技術剖析之十二:資料契約(Data Contract)和資料契約序列化器(DataContractSerializer)

WCF技術剖析之十三:序列化過程中的已知類型(Known Type)

WCF技術剖析之十四:泛型資料契約和集合資料契約(上篇)

WCF技術剖析之十四:泛型資料契約和集合資料契約(下篇)

WCF技術剖析之十五:資料契約代理(DataContractSurrogate)在序列化中的作用

WCF技術剖析之十六:資料契約的等效性和版本控制