WCF技术剖析之十二:数据契约(Data Contract)和数据契约序列化器(DataContractSerializer)
[爱心链接:拯救一个25岁身患急性白血病的女孩[内有苏州电视台经济频道《天天山海经》为此录制的节目视频(苏州话)]]大部分的系统都是以数据为中心的(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: }
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。