數據契約(Data Contract)和數據契約序列化器(DataContractSerializer)
大部分的系統都是以數據為中心的(Data Central),功能的實現表現在對相關數據的正確處理。而數據本身,是有效信息的載體,在不同的環境具有不同的表示。一個分布式的互聯系統關注於數據的交換,而數據正常交換的根本前提是參與數據交換的雙方對於數據結構的一致性理解。這就為數據的表現提出了要求,為了保證處於不同平台、不同廠商的應用能夠正常地進行數據交換,交換的數據必須采用一種大家都能夠理解的展現方式。在這方面,XML無疑是最好的選擇。所以WCF下的序列化(Serialization)解決的就是如何將數據從對象的表現形式轉變成XML表現形式,以確保數據的正常交換。從本章起,我將講述WCF序列化的本質,首先從從數據契約談起。
一、數據契約
一個正常的服務調用要求客戶端和服務端對服務操作有一致的理解,WCF通過服務契約對服務操作進行抽象,以一種與平台無關的,能夠被不同的廠商理解的方式對服務進行描述。同理,客戶端和服務端進行有效的數據交換,同樣要求交換雙方對交換數據的結構達成共識,WCF通過數據契約來對交換的數據進行描述。與數據契約的定義相匹配,WCF采用新的序列化器——數據契約序列化器(DataContractSerializer)進行基於數據契約的序列化於反序列化操作。
同服務契約類似,WCF采用了基於特性(Attribute)的數據契約定義方式。基於數據契約的自定義特性主要包含以下兩個:DataContractAttribute和DataMemberAttribute,接下來我們將討論這兩個重要的自定義特性。
DataContractAttribute和DataMemberAttribute
WCF通過應用DataContractAttribute特性將其目標類型定義成一個數據契約,下面是DataContractAttribute的定義。從AttributeUsage的定義來看,DataContractAttribute只能用於枚舉、類和結構體,而不能用於接口;DataContractAttribute是不可以被繼承的,也就是說當一個類型繼承了一個應用了DataContractAttribute特性類型,自身也只有顯式地應用DataContractAttribute特性才能成為數據契約;一個類型上只能應用唯一一個DataContractAttribute特性。
1: [AttributeUsage(AttributeTargets.Enum | AttributeTargets.Struct | AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
2: public sealed class DataContractAttribute : Attribute
3: {
4: public bool IsReference { get; set; }
5: public string Name { get; set; }
6: public string Namespace { get; set; }
7: }
DataContractAttribute僅僅包含3個屬性成員。其中Name和Namespace表示數據契約的名稱和命名空間;IsReference表示在進行序列化的時候是否保持對象現有的引用結構。比如說,一個對象的兩個屬性同時引用一個對象,那麼有兩個序列化方式,一種是在序列化後的XML仍然保留這種引用結構,另一種是將兩個屬性的值序列化成兩份獨立的具有相同內容的XML。
對於服務契約來說,我們在一個接口或者類上面應用的ServiceContractAttribute將其定義成服務契約後,並不意味著該接口或者類中的每一個方法成員都是服務操作,而是通過OperationContractAttribute顯式地將相應的方法定義成服務操作。與之類似,數據契約也采用這種顯式聲明的機制。對於應用了DataContractAttribute特性的類型,只有應用了DataMemberAttribute特性的字段或者屬性成員才能成為數據契約的數據成員。DataMemberAttribute特性的定義如下所示。
1: [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
2: public sealed class DataMemberAttribute : Attribute
3: {
4: public DataMemberAttribute();
5:
6: public bool EmitDefaultValue { get; set; }
7: public bool IsRequired { get; set; }
8: public string Name { get; set; }
9: public int Order { get; set; }
10: }
下面的列表列出了DataMemberAttribute的4個屬性所表述的含義。
Name:數據成員的名稱,默認為字段或者屬性的名稱;
Order:相應的數據成員在最終序列化後的XML出現的位置,Order值越小越靠前,默認值為-1;
IsRequired:表明屬性成員是否是必須的成員,默認值為false,表明該成員是可以缺省的;
EmitDefaultValue:表明在數據成員的值等於默認值的情況下,是否還需要將其序列化到最終的XML中,默認值為true,表示默認值會參與序列化。
注: 數據契約和數據成員只和是否應用了DataContractAttribute和DataMemberAttribute有關,與類型和成員的存取限制修飾符(public,internal、protected,private等)無關。也就是說,應用了DataMemberAttribute的私有字段或屬性成員也是數據契約的數據成員。
二、數據契約序列化器(DataContractSerializer)
在WCF中,數據契約的定義是為序列化和反序列化服務的。WCF采用數據契約序列化器(DataContractSerializer)作為默認的序列化器。接下來我們著重談談DataContractSerializer和基於DataContractSerializer采用的序列化規則。先來看看DataContractSerializer的定義。
1: public sealed class DataContractSerializer : XmlObjectSerializer
2: {
3: //其他成員
4: public DataContractSerializer(Type type);
5: //其他構造函數
6:
7: public override object ReadObject(XmlReader reader);
8: public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName);
9: public override object ReadObject(XmlReader reader, bool verifyObjectName);
10: public override void WriteObject(XmlWriter writer, object graph);
11:
12: public IDataContractSurrogate DataContractSurrogate { get; }
13: public bool IgnoreExtensionDataObject { get; }
14: public ReadOnlyCollection<Type> KnownTypes { get; }
15: public int MaxItemsInObjectGraph { get; }
16: public bool PreserveObjectReferences { get; }
17: }
DataContractSerializer定義了一系列的重載的構造函數,我們可以調用它們構建相應的DataContractSerializer對象,通過制定相應的參數控制系列化器的序列化和反序列化行為。在後續的介紹中我們會通過這些相應的構造函數創建DataContractSerializer對象,在這裡就不一一介紹了。DataContractSerializer主要通過兩個方法進行序列化和反序列化:WirteObject和ReadObject。這裡我們需要著重介紹一下DataContractSerializer的5個重要的屬性成員。
DataContractSurrogate:這是一個實現了IDataContractSurrogate接口的數據契約代理類的對象。契約代理會參與到DataContractSerializer的序列化、反序列化以及契約的導入和導出的過程中,實現對象和類型的替換;
IgnoreExtensionDataObject:擴展數據對象(ExtensionDataObject)旨在解決雙方數據契約不一致的情況下,在數據傳送-回傳(Round Trip)過程中造成的數據丟失;
KnownTypes:由於序列化和反序列化依賴於定義在類型的元數據信息,所以在進行序列化或者反序列化之前,需要確定被序列化對象,或者反序列化生成對象的所有相關的真實類型。為了確保序列化或反序列化的成功,須要相關的類型添加到KnownTypes類型集合中;
MaxItemsInObjectGraph:為了避免黑客生成較大數據,頻繁地訪問服務造成服務器不堪重負(我們一般把這種黑客行為稱為拒絕服務DoS-Denial of Service),可以通過MaxItemsInObjectGraph屬性設置進行序列化和反序列化允許的最大對象數。MaxItemsInObjectGraph的默認值為65536;
PreserveObjectReferences:這個屬性與DataContractAttribute的IsReference屬性的含義一樣,表示的是如果數據對象的多個屬性或者字段引用相同的對象,在序列化的時候是否需要在XML中保持一樣的引用結構。
三、基於DataContractSerializer的序列化規則
與在第一節介紹XmlSerializer的序列化規則一樣,現在我們通過一個具體的例子來介紹DataContractSerializer是如何進行序列化的,以及采用怎樣的序列化規則。我們照例定義一個泛型的輔助方法進行專門的序列化工作,最終生成的XML保存到一個XML文件中。
1: public static void Serialize<T>(T instance, string fileName)
2: {
3: DataContractSerializer serializer = new DataContractSerializer(typeof(T));
4: using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
5: {
6: serializer.WriteObject(writer, instance);
7: }
8: Process.Start(fileName);
9: }
我們需要對一個Order對象進行序列化,Order類型的定義如下。實際上我們定義了兩個數據契約:OrderBase和Order,Order繼承於OrderBase。
1: namespace Artech.DataContractSerializerDemos
2: {
3: [DataContract]
4: public class OrderBase
5: {
6: [DataMember]
7: public Guid ID
8: { get; set; }
9:
10: [DataMember]
11: public DateTime Date
12: { get; set; }
13:
14: [DataMember]
15: public string Customer
16: { get; set; }
17:
18: [DataMember]
19: public string ShipAddress
20: { get; set; }
21:
22: public double TotalPrice
23: { get; set; }
24: }
25:
26: [DataContract]
27: public class Order : OrderBase
28: {
29: [DataMember]
30: public string PaymentType
31: { get; set; }
32: }
33: }
通過下面的代碼對創建的Order對象進行序列化,會生成一段XML。
1: Order order = new Order()
2: {
3: ID = Guid.NewGuid(),
4: Date = DateTime.Today,
5: Customer = "NCS",
6: ShipAddress = "#328, Airport Rd, Industrial Park, Suzhou JiangSu Province",
7: TotalPrice = 8888,
8: PaymentType = "Credit Card"
9: };
10: Serialize(order, @"E:\order.xml");
1: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Artech.DataContractSerializerDemos">
2: <Customer>NCS</Customer>
3: <Date>2008-12-03T00:00:00+08:00</Date>
4: <ID>5fdbee36-e29e-48d2-b45f-6fd4beba54d6</ID>
5: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou JiangSu Province</ShipAddress>
6: <PaymentType>Credit Card</PaymentType>
7: </Order>
通過數據契約與最終生成的XML結構的對比,我們可以看出DataContractSerializer在默認的情況下采用如下的序列化規則:
XML的根節點名稱為數據契約類型的名稱,默認的命名空間采用這樣的格式:http://schemas.datacontract.org/2004/07/{數據契約類型的命名空間};
只有顯式應用了DataMemberAttribute特性的字段或者屬性才能作為數據成員采用才會參與序列化(比如TotalPrice屬性的值不會出現在序列化後的XML中);
所有數據成員均以XML元素的形式被序列化;
序列化後數據成員在XML的次序采用這樣的規則:父類數據成員在先,子類數據成員在後;定義在同一個類型的數據成員按照字母排序。
如果默認序列化後的XML結構不能滿足我們的要求,我們可以通過DataContractAttribute和DataMemberAttribute相應的屬性對其進行修正。在重新定義的數據契約中,我們通過DataContractAttribute設置了數據契約的名稱和命名空間;通過DataMemberAttribute的Name屬性為ID和Date兩個屬性設置了不同於屬性名稱的數據成員名稱,並通過Order控制了數據成員的先後次序。那麼調用相同的程序,最終被序列化出來的XML將會如下所示。
1: namespace Artech.DataContractSerializerDemos
2: {
3: [DataContract(Namespace="http://www.artech.com/")]
4: public class OrderBase
5: {
6: [DataMember(Name = "OrderID",Order=1)]
7: public Guid ID
8: { get; set; }
9:
10: [DataMember(Name = "OrderDate", Order = 2)]
11: public DateTime Date
12: { get; set; }
13:
14: [DataMember(Order = 3)]
15: public string Customer
16: { get; set; }
17:
18: [DataMember(Order = 4)]
19: public string ShipAddress
20: { get; set; }
21:
22: public double TotalPrice
23: { get; set; }
24: }
25:
26: [DataContract(Name="Ord", Namespace="http://www.artech.com/")]
27: public class Order : OrderBase
28: {
29: [DataMember(Order = 1)]
30: public string PaymentType
31: { get; set; }
32: }
33: }
1: <Ord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
2: <OrderID>ba3bc051-6c02-41dd-9f97-ae745ac5f1dd</OrderID>
3: <OrderDate>2008-12-03T00:00:00+08:00</OrderDate>
4: <Customer>NCS</Customer>
5: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou JiangSu Province</ShipAddress>
6: <PaymentType>Credit Card</PaymentType>
7: </Ord>
四、通過MaxItemsInObjectGraph限定序列化對象的數量
拒絕服務(DoS- Denial of Service)是一種常用的黑客攻擊行為,黑客通過生成大容量的數據頻繁地對服務器發送請求,最終導致服務器不堪重負而崩潰。對於WCF的序列化或反序列化來說,數據的容量越大、成員越多、層次越深,序列化的時間就越長,耗用的資源就越多,如果黑客頻繁地發送一個海量的數組過來,那麼服務就會因為忙於進行反序列化的工作而沒有足夠的資源處理正常的請求,從而導致癱瘓。
在這種情況下,我們可以通過MaxItemsInObjectGraph這個屬性設置DataContractSerializer允許被序列化或者反序列化對象數量的上限。一旦超過這個設定上限,序列化或者反序列化的工作將會立即中止,從而在一定程度上解決了通過發送大集合數據形式的拒絕服務攻擊。DataContractSerializer中定義了以下3個重載的構造函數使我們能夠設定MaxItemsInObjectGraph屬性。
1: public sealed class DataContractSerializer : XmlObjectSerializer
2: {
3: //其他成員
4: public DataContractSerializer(Type type, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
5:
6: public DataContractSerializer(Type type, string rootName, string rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
7:
8: public DataContractSerializer(Type type, XmlDictionaryString rootName, XmlDictionaryString rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
9:
10: public int MaxItemsInObjectGraph { get; }
11: }
那麼DataContractSerializer在進行具體的序列化的時候,對象的個數如何計算呢?經過我的實驗,發現采用的計算規則是這樣的:對象自身算一個對象,對於所有成員以及所有內嵌的成員都算一個對象。我們通過一個具體的例子來證實這一點,在上面定義的范型Serialize方法上面,我加了另一個參數maxItemsInObjectGraph,並調用另一個構造函數來創建DataContractSerializer對象。
1: public static void Serialize<T>(T instance, string fileName, int maxItemsInObjectGraph)
2: {
3: DataContractSerializer serializer = new DataContractSerializer(typeof(T),null,maxItemsInObjectGraph,false,false,null);
4: using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
5: {
6: serializer.WriteObject(writer, instance);
7: }
8: Process.Start(fileName);
9: }
我們現在准備調用上面的方法對一個集合對象進行序列化,為此我定義了一個OrderCollection的類型,它直接繼承了List<Order>。
1: public class OrderCollection : List<Order>
2: { }
3:
4: [DataContract]
5: public class Order
6: {
7: [DataMember]
8: public Guid ID
9: { get; set; }
10:
11: [DataMember]
12: public DateTime Date
13: { get; set; }
14:
15: [DataMember]
16: public string Customer
17: { get; set; }
18:
19: [DataMember]
20: public string ShipAddress
21: { get; set; }
22: }
在下面的代碼中,創建了OrderCollection對象,並添加了10個Order對象,如果該對象被序列化,最終被序列化對象數量是多少呢?應該這樣來算,OrderCollection對象本身算一個,每一個Order對象自身也算一個,Order對象具有4個屬性,各算一個,那麼最終計算出來的個數是10×5+1=51個。但是在調用Serialize方法的時候,我僅僅指定的上限是10×5=50。所有當調用DataContractSerializer的WriteObject方法的時候,會拋出如圖1所示的SerializationException異常。如果maxItemsInObjectGraph設為51則一切正常。
1: OrderCollection orders = new OrderCollection();
2: for (int i = 0; i < 10; i++)
3: {
4: Order order = new Order()
5: {
6: ID = Guid.NewGuid(),
7: Date = DateTime.Today,
8: Customer = "NCS",
9: ShipAddress = "#328, Airport Rd, Industrial Park, Suzhou JiangSu Province",
10: };
11: orders.Add(order);
12: }
13: Serialize(orders, @"E:\order.xml", 10*5);
圖1 序列化對象數量超出maxItemsInObjectGraph導致的序列化異常
在WCF應用中,MaxItemsInObjectGraph的值可以通過ServiceBehaviorAttribute進行設置,在下面的代碼中,為OrderService的MaxItemsInObjectGraph設為51。
1: [ServiceBehavior(MaxItemsInObjectGraph = 51)]
2: public class OrderService : IOrder
3: {
4: public void ProcessOrder(OrderCollection orders)
5: {
6: //省略實現
7: }
8: }
MaxItemsInObjectGraph也可以通過配置方式進行設置,MaxItemsInObjectGraph通過serviceBehavior的dataContractSerializer配置項進行設置。
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <behaviors>
5: <serviceBehaviors>
6: <behavior name="serializationLimitationBehavior">
7: <dataContractSerializer maxItemsInObjectGraph="51" />
8: </behavior>
9: </serviceBehaviors>
10: </behaviors>
11: <services>
12: <service behaviorConfiguration="serializationLimitationBehavior" name="OrderService">
13: <endpoint address="http://127.0.0.1:9999/orderservice" binding="basicHttpBinding" bindingConfiguration="" contract="IOrder" />
14: </service>
15: </services>
16: </system.serviceModel>
17: </configuration>
五、如何保持對象現有的引用結構
數據類型有值類型和引用類型之分,那麼對於一個數據契約類型對象,如果多個數據成員同時引用同一個對象,那應該采用怎樣的序列化規則呢?是保留現有的引用結構呢,還是將它們序列化成具有相同內容的XML片斷。DataContractSerializer的這種特性通過只讀屬性PreserveObjectReferences 表示,默認值為false,所以在默認的情況下采用的是後一種序列化方式。DataContractSerializer定義了以下3個重載的構造函數,供我們顯式地指定該參數。
1: public sealed class DataContractSerializer : XmlObjectSerializer
2: {
3: //其他成員
4: public DataContractSerializer(Type type, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
5:
6: public DataContractSerializer(Type type, string rootName, string rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
7:
8: public DataContractSerializer(Type type, XmlDictionaryString rootName, XmlDictionaryString rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
9:
10: public bool PreserveObjectReferences { get; }
11: }
我們通過下面一個簡單的例子來看看對於一個數據契約對象,在保留對象引用和不保留引用的情況下,序列化出來的XML到底有什麼不同的地方。在這裡需要對上面定義的泛型輔助的Serialize<T>方法作相應的修正,加入preserveObjectReferences參數,並通過該參數創建相應的DataContractSerializer對象。
1: static void Serialize<T>(T instance, string fileName, bool preserveReference)
2: {
3: DataContractSerializer serializer = new DataContractSerializer(typeof(T), null, int.MaxValue, false, preserveReference, null);
4: using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
5: {
6: serializer.WriteObject(writer, instance);
7: }
8: Process.Start(fileName);
9: }
我們的例子需要對一個Customer對象進行序列化,Customer的定義如下。需要注意的是Customer類中定義了兩個屬性:CompanyAddress和ShipAddress,它們的類型均為Address。
1: namespace Artech.DataContractSerializerDemos
2: {
3: [DataContract]
4: public class Customer
5: {
6: [DataMember]
7: public string Name
8: { get; set; }
9:
10: [DataMember]
11: public string Phone
12: { get; set; }
13:
14: [DataMember]
15: public Address CompanyAddress
16: { get; set; }
17:
18: [DataMember]
19: public Address ShipAddress
20: { get; set; }
21: }
22:
23: [DataContract]
24: public class Address
25: {
26: [DataMember]
27: public string Province
28: { get; set; }
29:
30: [DataMember]
31: public string City
32: { get; set; }
33:
34: [DataMember]
35: public string District
36: { get; set; }
37:
38: [DataMember]
39: public string Road
40: { get; set; }
41: }
42: }
現在我們創建Customer對象,讓CompanyAddress和ShipAddress屬性引用同一個Address對象,先後通過Serialize<T>方法,並將參數preserveReference分別設置為false和true。
1: Address address = new Address()
2: {
3: Province = "Jiang Su",
4: City = "Su Zhou",
5: District = "Industrial Park",
6: Road = "Airport Rd #328"
7: };
8:
9: Customer customer = new Customer()
10: {
11: Name = "Foo",
12: Phone = "8888-88888888",
13: ShipAddress = address,
14: CompanyAddress = address
15: };
16:
17: Serialize<Customer>(customer,@"E:\customer1.xml", false);
18: Serialize<Customer>(customer, @"E:\customer2.xml", true);
下面兩段XML片斷分別表示兩次序列化生成的XML。我們可以很明顯地看出,在不保留對象引用的情況下,CompanyAddress和ShipAddress對應著兩段具有相同內容的XML片斷,而在保留對象引用的情況下,它們則是引用同一個XML元素。
1: <Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Artech.DataContractSerializerDemos">
2: <CompanyAddress>
3: <City>Su Zhou</City>
4: <District>Industrial Park</District>
5: <Province>Jiang Su</Province>
6: <Road>Airport Rd #328</Road>
7: </CompanyAddress>
8: <Name>Foo</Name>
9: <Phone>8888-88888888</Phone>
10: <ShipAddress>
11: <City>Su Zhou</City>
12: <District>Industrial Park</District>
13: <Province>Jiang Su</Province>
14: <Road>Airport Rd #328</Road>
15: </ShipAddress>
16: </Customer>
1: <Customer xmlns:i="http://www.w3.org/2001/XMLSchema-instance" z:Id="1" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns="http://schemas.datacontract.org/2004/07/Artech.DataContractSerializerDemos">
2: <CompanyAddress z:Id="2">
3: <City z:Id="3">Su Zhou</City>
4: <District z:Id="4">Industrial Park</District>
5: <Province z:Id="5">Jiang Su</Province>
6: <Road z:Id="6">Airport Rd #328</Road>
7: </CompanyAddress>
8: <Name z:Id="7">Foo</Name>
9: <Phone z:Id="8">8888-88888888</Phone>
10: <ShipAddress z:Ref="2" i:nil="true" />
11: </Customer>
前面介紹DataContractAttribute的時候,我們說到DataContractAttribute的屬性IsReference起到PreserveObjectReferences屬性一樣的作用。在對DataContractSerializer的PreserveReference屬性沒有顯式設置的情況下,將應用在Address上的DataContractAttribute的IsReference屬性設為true,同樣可以實現保留對象引用的目的。
1: [DataContract(IsReference = true)]
2: public class Address
3: {
4: //類型成員
5: }