Windows Communication Foundation (WCF) 包括新的序列化引擎 ,即 DataContractSerializer. DataContractSerializer 在 .NET Framework 对象和 XML 之间进行双向转换。 本主题介绍序列化程序的工作原理。
序列化 .NET Framework 对象时,序列化程序会了解各种序列化编程模型,包括新的 数据协定 模型。 有关支持类型的完整列表,请参阅 数据协定序列化程序支持的类型。 有关数据协定的简介,请参阅 “使用数据协定”。
反序列化 XML 时,序列化程序使用 XmlReader 和 XmlWriter 类。 它还支持 XmlDictionaryReader 和 XmlDictionaryWriter 类,以便在某些情况下生成优化的 XML,例如在使用 WCF 二进制 XML 格式时。
WCF 还包括配套序列化程序,即 NetDataContractSerializer. NetDataContractSerializer:
- 不安全。 有关详细信息,请参阅 BinaryFormatter 安全指南。
- 与BinaryFormatterSoapFormatter序列化程序类似,因为它还会作为序列化数据的一部分发出 .NET Framework 类型名称。
- 当在序列化和反序列化结束阶段共享相同的类型时使用。
DataContractSerializer和NetDataContractSerializer都派生自一个公共基类XmlObjectSerializer。
警告
DataContractSerializer 将包含带小于 20 的十六进制值的控制字符序列化为 XML 实体。 向 WCF 服务发送此类数据时,这可能会导致非 WCF 客户端出现问题。
创建 DataContractSerializer 实例
构造实例 DataContractSerializer 是一个重要步骤。 构造后,无法更改任何设置。
指定根类型
根类型是序列化或反序列化实例的类型。
DataContractSerializer 有许多构造函数重载,但至少必须通过 type
参数提供一个根类型。
为特定根类型创建的序列化程序不能用于序列化(或反序列化)另一种类型,除非该类型派生自根类型。 以下示例演示两个类。
[DataContract]
public class Person
{
// Code not shown.
}
[DataContract]
public class PurchaseOrder
{
// Code not shown.
}
<DataContract()> _
Public Class Person
' Code not shown.
End Class
<DataContract()> _
Public Class PurchaseOrder
' Code not shown.
End Class
此代码构造一个实例,该实例 DataContractSerializer
只能用于序列化或反序列化类的 Person
实例。
DataContractSerializer dcs = new DataContractSerializer(typeof(Person));
// This can now be used to serialize/deserialize Person but not PurchaseOrder.
Dim dcs As New DataContractSerializer(GetType(Person))
' This can now be used to serialize/deserialize Person but not PurchaseOrder.
指定已知类型
如果多态性涉及被序列化的类型,而这些类型尚未通过使用 KnownTypeAttribute 属性或其他机制处理,则必须使用 knownTypes
参数将可能的已知类型列表传递给序列化程序的构造函数。 有关已知类型的详细信息,请参阅 数据协定已知类型。
以下示例显示了一个类, LibraryPatron
该类包含特定类型的 LibraryItem
集合。 第二类定义类型 LibraryItem
。 第三个和四个类(Book
和 Newspaper
)继承自该 LibraryItem
类。
[DataContract]
public class LibraryPatron
{
[DataMember]
public LibraryItem[] borrowedItems;
}
[DataContract]
public class LibraryItem
{
// Code not shown.
}
[DataContract]
public class Book : LibraryItem
{
// Code not shown.
}
[DataContract]
public class Newspaper : LibraryItem
{
// Code not shown.
}
<DataContract()> _
Public Class LibraryPatron
<DataMember()> _
Public borrowedItems() As LibraryItem
End Class
<DataContract()> _
Public Class LibraryItem
' Code not shown.
End Class
<DataContract()> _
Public Class Book
Inherits LibraryItem
' Code not shown.
End Class
<DataContract()> _
Public Class Newspaper
Inherits LibraryItem
' Code not shown.
End Class
以下代码使用 knownTypes
参数构造序列化程序的实例。
// Create a serializer for the inherited types using the knownType parameter.
Type[] knownTypes = new Type[] { typeof(Book), typeof(Newspaper) };
DataContractSerializer dcs =
new DataContractSerializer(typeof(LibraryPatron), knownTypes);
// All types are known after construction.
' Create a serializer for the inherited types using the knownType parameter.
Dim knownTypes() As Type = {GetType(Book), GetType(Newspaper)}
Dim dcs As New DataContractSerializer(GetType(LibraryPatron), knownTypes)
' All types are known after construction.
指定默认根名称和命名空间
通常,在序列化对象时,根据数据协定名称和命名空间确定最外部 XML 元素的默认名称和命名空间。 所有内部元素的名称都根据数据成员名称确定,其命名空间是数据协定的命名空间。 以下示例在Name
和Namespace
类的构造函数中设置DataContractAttribute和DataMemberAttribute值。
[DataContract(Name = "PersonContract", Namespace = "http://schemas.contoso.com")]
public class Person2
{
[DataMember(Name = "AddressMember")]
public Address theAddress;
}
[DataContract(Name = "AddressContract", Namespace = "http://schemas.contoso.com")]
public class Address
{
[DataMember(Name = "StreetMember")]
public string street;
}
<DataContract(Name:="PersonContract", [Namespace]:="http://schemas.contoso.com")> _
Public Class Person2
<DataMember(Name:="AddressMember")> _
Public theAddress As Address
End Class
<DataContract(Name:="AddressContract", [Namespace]:="http://schemas.contoso.com")> _
Public Class Address
<DataMember(Name:="StreetMember")> _
Public street As String
End Class
序列化 Person
类的实例将生成类似于以下内容的 XML。
<PersonContract xmlns="http://schemas.contoso.com">
<AddressMember>
<StreetMember>123 Main Street</StreetMember>
</AddressMember>
</PersonContract>
但是,可以通过将 rootName
和 rootNamespace
参数的值传递给 DataContractSerializer 构造函数,用于自定义根元素的默认名称和命名空间。 请注意,这 rootNamespace
不会影响与数据成员对应的包含元素的命名空间。 它仅影响最外部元素的命名空间。
这些值可以作为类的 XmlDictionaryString 字符串或实例传递,以允许使用二进制 XML 格式进行优化。
设置最大对象配额
某些 DataContractSerializer
构造函数重载具有参数 maxItemsInObjectGraph
。 此参数确定序列化程序在单个 ReadObject 方法调用中序列化或反序列化的最大对象数。 (该方法始终读取一个根对象,但此对象在其数据成员中可能具有其他对象。这些对象可能具有其他对象,等等。默认值为 65536。 请注意,序列化或反序列化数组时,每个数组条目都算作单独的对象。 此外,请注意,某些对象可能有较大的内存表示形式,因此仅此配额可能不足以防止拒绝服务攻击。 有关详细信息,请参阅 数据的安全注意事项。 如果需要将此配额提高到默认值之外,请务必在发送(序列化)和接收(反序列化)端上执行此作,因为它适用于读取和写入数据时。
往返行程
在一次操作中对对象进行反序列化和重新序列化时将发生往返行程 。 因此,它从 XML 到对象实例,并再次返回到 XML 流中。
某些 DataContractSerializer
构造函数重载具有一个 ignoreExtensionDataObject
参数,该参数默认设置为 false
。 在此默认模式下,只要数据协定实现 IExtensibleDataObject 接口,就可以通过较旧版本从较新版本的数据协定往返发送数据,并返回到较新版本,而不会丢失数据。 例如,假设版本 1 的 Person
数据协定包含 Name
和 PhoneNumber
数据成员,而版本 2 添加了一个 Nickname
成员。 如果 IExtensibleDataObject
实现,则在将信息从版本 2 发送到版本 1 时存储 Nickname
数据,然后在数据再次序列化时重新发出;因此,在往返中不会丢失任何数据。 有关详细信息,请参阅 Forward-Compatible 数据协定 和数据 协定版本控制。
往返行程的安全性和架构有效性问题
往返可能会造成安全影响。 例如,反序列化和存储大量多余的数据可能是安全风险。 可能重新发出这些数据存在安全隐患,尤其是在涉及数字签名时,因为无法进行验证。 例如,在先前的场景中,版本 1 终结点可能会签署包含恶意数据的 Nickname
值。 最后,可能存在架构有效性问题:终结点可能希望始终发出严格遵循其规定协定的数据,而不发出任何额外值。 在前面的示例中,版本 1 终结点的协定表示它只发出 Name
和 PhoneNumber
,并且如果使用架构验证,发出额外的 Nickname
值会导致验证失败。
启用和禁用往返行程
要关闭往返行程,请不要实现 IExtensibleDataObject 接口。 如果无法控制类型,请将 ignoreExtensionDataObject
参数设置为 true
达到相同的效果。
对象图保留
通常,序列化程序不关心对象标识,如以下代码所示。
[DataContract]
public class PurchaseOrder
{
[DataMember]
public Address billTo;
[DataMember]
public Address shipTo;
}
[DataContract]
public class Address
{
[DataMember]
public string street;
}
<DataContract()> _
Public Class PurchaseOrder
<DataMember()> _
Public billTo As Address
<DataMember()> _
Public shipTo As Address
End Class
<DataContract()> _
Public Class Address
<DataMember()> _
Public street As String
End Class
以下代码创建采购订单。
// Construct a purchase order:
Address adr = new Address();
adr.street = "123 Main St.";
PurchaseOrder po = new PurchaseOrder();
po.billTo = adr;
po.shipTo = adr;
' Construct a purchase order:
Dim adr As New Address()
adr.street = "123 Main St."
Dim po As New PurchaseOrder()
po.billTo = adr
po.shipTo = adr
请注意,billTo
字段和 shipTo
字段被设置为同一个对象实例。 但是,生成的 XML 复制重复的信息,看起来类似于以下 XML。
<PurchaseOrder>
<billTo><street>123 Main St.</street></billTo>
<shipTo><street>123 Main St.</street></shipTo>
</PurchaseOrder>
但是,此方法具有以下特征,这可能不可取:
性能。 复制数据效率低下。
循环引用。 如果对象引用自身,甚至通过其他对象引用自身,则通过复制进行序列化会导致无限循环。 (如果发生这种状况,序列化程序将引发 SerializationException 。)
语义学。 有时,保持对两个引用指向同一对象这一事实的重视很重要,而不是指向两个相同的对象。
出于这些原因,某些 DataContractSerializer
构造函数重载具有参数 preserveObjectReferences
(默认值为 false
)。 当此参数设置为 true
时,将使用仅使用 WCF 理解的特殊编码对象引用方法。 如果设置为 true
,则 XML 代码示例现在类似于以下内容。
<PurchaseOrder ser:id="1">
<billTo ser:id="2"><street ser:id="3">123 Main St.</street></billTo>
<shipTo ser:ref="2"/>
</PurchaseOrder>
“ser”命名空间是指标准序列化命名空间 http://schemas.microsoft.com/2003/10/Serialization/
。 每个数据片段仅序列化一次,并给定 ID 号,后续使用会导致对已序列化数据的引用。
重要
如果数据协定 XMLElement
中同时存在“id”和“ref”属性,则会遵循“ref”属性,并忽略“id”属性。
了解此模式的限制非常重要:
DataContractSerializer
在preserveObjectReferences
设置为true
的情况下生成的 XML 与任何其他技术都无法进行交互,仅可以由另一个其DataContractSerializer
也设置为preserveObjectReferences
的true
实例进行访问。此功能没有元数据(架构)支持。 生成的架构仅在
preserveObjectReferences
设置为false
时有效。此功能可能会导致序列化和反序列化过程运行速度较慢。 尽管无需复制数据,但必须在此模式下执行额外的对象比较。
谨慎
在启用preserveObjectReferences
模式时,尤其重要的是将maxItemsInObjectGraph
值设置为正确的配额。 由于在此模式下处理数组的方式,攻击者很容易构造一条小型恶意消息,从而导致内存消耗巨大,仅受 maxItemsInObjectGraph
配额的限制。
指定数据契约代理
某些 DataContractSerializer
构造函数重载具有一个 dataContractSurrogate
参数,该参数可以设置为 null
。 否则,可以使用它来指定 数据协定代理项,这是实现 IDataContractSurrogate 接口的类型。 然后,可以使用接口自定义序列化和反序列化过程。 有关详细信息,请参阅 数据契约代理。
序列化
以下信息适用于继承自 XmlObjectSerializer 的任何类,包括 DataContractSerializer 和 NetDataContractSerializer 类。
简单序列化
序列化对象的最基本方法是将其 WriteObject 传递给方法。 该方法有三个重载,每个重载分别用于写入到 Stream、 XmlWriter或 XmlDictionaryWriter。 使用 Stream 重载时,输出为 UTF-8 编码中的 XML。 XmlDictionaryWriter重载时,序列化程序会优化其输出以适应二进制 XML。
使用 WriteObject 该方法时,序列化程序使用包装元素的默认名称和命名空间,并将其与内容一起写入(请参阅前面的“指定默认根名称和命名空间”部分)。
以下示例演示了如何用XmlDictionaryWriter进行写作。
Person p = new Person();
DataContractSerializer dcs =
new DataContractSerializer(typeof(Person));
XmlDictionaryWriter xdw =
XmlDictionaryWriter.CreateTextWriter(someStream,Encoding.UTF8 );
dcs.WriteObject(xdw, p);
Dim p As New Person()
Dim dcs As New DataContractSerializer(GetType(Person))
Dim xdw As XmlDictionaryWriter = _
XmlDictionaryWriter.CreateTextWriter(someStream, Encoding.UTF8)
dcs.WriteObject(xdw, p)
这会生成类似于以下内容的 XML。
<Person>
<Name>Jay Hamlin</Name>
<Address>123 Main St.</Address>
</Person>
分步引导的序列化
使用WriteStartObject、WriteObjectContent和WriteEndObject方法分别写入结束元素、写入对象内容,并关闭包装元素。
注释
这些方法没有 Stream 重载。
此分步序列化有两个常见用途。 一个是插入属性或注释等WriteStartObject
WriteObjectContent
内容,如以下示例所示。
dcs.WriteStartObject(xdw, p);
xdw.WriteAttributeString("serializedBy", "myCode");
dcs.WriteObjectContent(xdw, p);
dcs.WriteEndObject(xdw);
dcs.WriteStartObject(xdw, p)
xdw.WriteAttributeString("serializedBy", "myCode")
dcs.WriteObjectContent(xdw, p)
dcs.WriteEndObject(xdw)
这会生成类似于以下内容的 XML。
<Person serializedBy="myCode">
<Name>Jay Hamlin</Name>
<Address>123 Main St.</Address>
</Person>
另一个常见用途是完全避免使用 WriteStartObject 和 WriteEndObject,改为编写自己的自定义包装器元素(甚至可以完全跳过编写包装器),如以下代码所示。
xdw.WriteStartElement("MyCustomWrapper");
dcs.WriteObjectContent(xdw, p);
xdw.WriteEndElement();
xdw.WriteStartElement("MyCustomWrapper")
dcs.WriteObjectContent(xdw, p)
xdw.WriteEndElement()
这会生成类似于以下内容的 XML。
<MyCustomWrapper>
<Name>Jay Hamlin</Name>
<Address>123 Main St.</Address>
</MyCustomWrapper>
注释
使用分步引导的序列化可能会导致架构无效的 XML。
反序列化
以下信息适用于继承自 XmlObjectSerializer 的任何类,包括 DataContractSerializer 和 NetDataContractSerializer 类。
反序列化对象的最基本方法是调用其中一个ReadObject方法重载。 该方法有三个重载,每个重载分别用于读取 XmlDictionaryReader、 XmlReader
或 Stream
。 请注意, Stream
重载会创建不受任何配额保护的文本 XmlDictionaryReader ,并且只应用于读取受信任的数据。
另请注意,ReadObject
方法返回的对象必须被强制转换为适当的类型。
下面的代码构造一个实例 DataContractSerializer , XmlDictionaryReader然后反序列化一个 Person
实例。
DataContractSerializer dcs = new DataContractSerializer(typeof(Person));
FileStream fs = new FileStream(path, FileMode.Open);
XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas());
Person p = (Person)dcs.ReadObject(reader);
Dim dcs As New DataContractSerializer(GetType(Person))
Dim fs As New FileStream(path, FileMode.Open)
Dim reader As XmlDictionaryReader = _
XmlDictionaryReader.CreateTextReader(fs, New XmlDictionaryReaderQuotas())
Dim p As Person = CType(dcs.ReadObject(reader), Person)
在调用 ReadObject 该方法之前,将 XML 读取器放在包装元素或包装元素之前的非内容节点上。 可以通过调用 Read 或其派生项的 XmlReader 方法并测试 NodeType来完成此操作,如以下代码所示。
DataContractSerializer ser = new DataContractSerializer(typeof(Person),
"Customer", @"http://www.contoso.com");
FileStream fs = new FileStream(path, FileMode.Open);
XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas());
while (reader.Read())
{
switch (reader.NodeType)
{
case XmlNodeType.Element:
if (ser.IsStartObject(reader))
{
Console.WriteLine("Found the element");
Person p = (Person)ser.ReadObject(reader);
Console.WriteLine($"{p.Name} {p.Address} id:{2}");
}
Console.WriteLine(reader.Name);
break;
}
}
Dim ser As New DataContractSerializer(GetType(Person), "Customer", "http://www.contoso.com")
Dim fs As New FileStream(path, FileMode.Open)
Dim reader As XmlDictionaryReader = XmlDictionaryReader.CreateTextReader(fs, New XmlDictionaryReaderQuotas())
While reader.Read()
Select Case reader.NodeType
Case XmlNodeType.Element
If ser.IsStartObject(reader) Then
Console.WriteLine("Found the element")
Dim p As Person = CType(ser.ReadObject(reader), Person)
Console.WriteLine("{0} {1}", _
p.Name, p.Address)
End If
Console.WriteLine(reader.Name)
End Select
End While
请注意,您可以在将读取器交给ReadObject
之前读取此包装元素上的属性。
使用简单 ReadObject
重载之一时,反序列化程序会在包装元素上查找默认名称和命名空间(请参阅上一节“指定默认根名称和命名空间”),如果找到未知元素,则会引发异常。 在前面的示例中,<Person>
包装元素是预期的。 调用 IsStartObject 该方法以验证读取器是否位于按预期命名的元素上。
有一种方法可以禁用此包装元素名称检查; ReadObject
方法的某些重载采用布尔参数 verifyObjectName
,该参数默认设置为 true
。 设置为 false
时,将忽略包装元素的名称和命名空间。 这对于读取使用前面所述的分步序列化机制编写的 XML 非常有用。
使用 NetDataContractSerializer
DataContractSerializer
和NetDataContractSerializer的主要区别在于,DataContractSerializer
使用数据协定名称,而NetDataContractSerializer
在序列化的 XML 中输出完整的 .NET Framework 程序集和类型名称。 这意味着必须在序列化和反序列化终结点之间共享完全相同的类型。 这意味着,由于要反序列化的确切类型始终是已知的,因此不需要已知类型机制 NetDataContractSerializer
。
但是,可能会出现以下几个问题:
安全性。 在反序列化的 XML 中找到的任何类型都会被加载。 这可以被利用来强制加载恶意类型。 仅在使用
NetDataContractSerializer
序列化联编程序 时(使用 属性或构造函数参数)才应将 Binder 用于不受信任的数据。 联编程序仅允许加载安全类型。 联编程序机制与 System.Runtime.Serialization 命名空间中的类型使用的机制相同。版本控制。 在 XML 中使用完整类型和程序集名称会严重限制如何对类型进行版本控制。 无法更改以下内容:类型名称、命名空间、程序集名称和程序集版本。 将 AssemblyFormat 属性或构造函数参数设置为 Simple 而不是默认值 Full ,允许程序集版本更改,但不适用于泛型参数类型。
互作性。 由于 .NET Framework 类型和程序集名称包含在 XML 中,因此 .NET Framework 以外的平台无法访问生成的数据。
性能。 写出类型和程序集名称会显著增加生成的 XML 的大小。
.NET Framework 远程处理使用的二进制或 SOAP 序列化机制与此类似(尤其是 BinaryFormatter 和 SoapFormatter)。
使用方法 NetDataContractSerializer
与使用 DataContractSerializer
方法类似,但存在以下差异:
构造函数不需要指定根类型。 可以使用
NetDataContractSerializer
的同一实例序列化任何类型。构造函数不接受已知类型的列表。 如果类型名称序列化为 XML,则不需要已知类型机制。
构造函数不接受数据协定代理项, 相反,它们接受一个名为ISurrogateSelector的
surrogateSelector
参数(该参数映射到SurrogateSelector属性)。 这是一种旧代理机制。构造函数接受
assemblyFormat
的一个名为 FormatterAssemblyStyle 的参数,该参数映射到 AssemblyFormat 属性。 如前所述,这可用于增强序列化程序的版本控制功能。 这与 FormatterAssemblyStyle 二进制或 SOAP 序列化中的机制相同。构造函数接受一个称为StreamingContext 的
context
参数,该参数映射到Context属性。 可以使用该参数将信息传递到要序列化的类型中。 此用法与其他 StreamingContext 类中使用的 System.Runtime.Serialization 机制相同。Serialize方法和Deserialize方法是WriteObject方法和ReadObject方法的别名。 这些是为了提供一个与二进制或 SOAP 序列化更一致的编程模型而存在的。
有关这些功能的详细信息,请参阅 二进制序列化。
NetDataContractSerializer
和 DataContractSerializer
所使用的 XML 格式通常不兼容。 也就是说,尝试使用其中一个序列化程序进行序列化,并使用另一个序列化程序进行反序列化不是受支持的方案。
此外,请注意,NetDataContractSerializer
不会输出对象图中每个节点的完整 .NET Framework 类型和程序集名称。 只有在信息不明确时才输出。 也就是说,它是在根对象级别进行输出并且是针对任何多态情况。