序列化和反序列化

Windows Communication Foundation (WCF) 包括新的序列化引擎 ,即 DataContractSerializer. DataContractSerializer 在 .NET Framework 对象和 XML 之间进行双向转换。 本主题介绍序列化程序的工作原理。

序列化 .NET Framework 对象时,序列化程序会了解各种序列化编程模型,包括新的 数据协定 模型。 有关支持类型的完整列表,请参阅 数据协定序列化程序支持的类型。 有关数据协定的简介,请参阅 “使用数据协定”。

反序列化 XML 时,序列化程序使用 XmlReaderXmlWriter 类。 它还支持 XmlDictionaryReaderXmlDictionaryWriter 类,以便在某些情况下生成优化的 XML,例如在使用 WCF 二进制 XML 格式时。

WCF 还包括配套序列化程序,即 NetDataContractSerializer. NetDataContractSerializer

  • 不安全。 有关详细信息,请参阅 BinaryFormatter 安全指南
  • BinaryFormatterSoapFormatter序列化程序类似,因为它还会作为序列化数据的一部分发出 .NET Framework 类型名称。
  • 当在序列化和反序列化结束阶段共享相同的类型时使用。

DataContractSerializerNetDataContractSerializer都派生自一个公共基类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 。 第三个和四个类(BookNewspaper)继承自该 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 元素的默认名称和命名空间。 所有内部元素的名称都根据数据成员名称确定,其命名空间是数据协定的命名空间。 以下示例在NameNamespace类的构造函数中设置DataContractAttributeDataMemberAttribute值。

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

但是,可以通过将 rootNamerootNamespace 参数的值传递给 DataContractSerializer 构造函数,用于自定义根元素的默认名称和命名空间。 请注意,这 rootNamespace 不会影响与数据成员对应的包含元素的命名空间。 它仅影响最外部元素的命名空间。

这些值可以作为类的 XmlDictionaryString 字符串或实例传递,以允许使用二进制 XML 格式进行优化。

设置最大对象配额

某些 DataContractSerializer 构造函数重载具有参数 maxItemsInObjectGraph 。 此参数确定序列化程序在单个 ReadObject 方法调用中序列化或反序列化的最大对象数。 (该方法始终读取一个根对象,但此对象在其数据成员中可能具有其他对象。这些对象可能具有其他对象,等等。默认值为 65536。 请注意,序列化或反序列化数组时,每个数组条目都算作单独的对象。 此外,请注意,某些对象可能有较大的内存表示形式,因此仅此配额可能不足以防止拒绝服务攻击。 有关详细信息,请参阅 数据的安全注意事项。 如果需要将此配额提高到默认值之外,请务必在发送(序列化)和接收(反序列化)端上执行此作,因为它适用于读取和写入数据时。

往返行程

在一次操作中对对象进行反序列化和重新序列化时将发生往返行程 。 因此,它从 XML 到对象实例,并再次返回到 XML 流中。

某些 DataContractSerializer 构造函数重载具有一个 ignoreExtensionDataObject 参数,该参数默认设置为 false。 在此默认模式下,只要数据协定实现 IExtensibleDataObject 接口,就可以通过较旧版本从较新版本的数据协定往返发送数据,并返回到较新版本,而不会丢失数据。 例如,假设版本 1 的 Person 数据协定包含 NamePhoneNumber 数据成员,而版本 2 添加了一个 Nickname 成员。 如果 IExtensibleDataObject 实现,则在将信息从版本 2 发送到版本 1 时存储 Nickname 数据,然后在数据再次序列化时重新发出;因此,在往返中不会丢失任何数据。 有关详细信息,请参阅 Forward-Compatible 数据协定 和数据 协定版本控制

往返行程的安全性和架构有效性问题

往返可能会造成安全影响。 例如,反序列化和存储大量多余的数据可能是安全风险。 可能重新发出这些数据存在安全隐患,尤其是在涉及数字签名时,因为无法进行验证。 例如,在先前的场景中,版本 1 终结点可能会签署包含恶意数据的 Nickname 值。 最后,可能存在架构有效性问题:终结点可能希望始终发出严格遵循其规定协定的数据,而不发出任何额外值。 在前面的示例中,版本 1 终结点的协定表示它只发出 NamePhoneNumber,并且如果使用架构验证,发出额外的 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”属性。

了解此模式的限制非常重要:

  • DataContractSerializerpreserveObjectReferences 设置为 true 的情况下生成的 XML 与任何其他技术都无法进行交互,仅可以由另一个其 DataContractSerializer 也设置为 preserveObjectReferencestrue实例进行访问。

  • 此功能没有元数据(架构)支持。 生成的架构仅在preserveObjectReferences设置为false时有效。

  • 此功能可能会导致序列化和反序列化过程运行速度较慢。 尽管无需复制数据,但必须在此模式下执行额外的对象比较。

谨慎

在启用preserveObjectReferences模式时,尤其重要的是将maxItemsInObjectGraph值设置为正确的配额。 由于在此模式下处理数组的方式,攻击者很容易构造一条小型恶意消息,从而导致内存消耗巨大,仅受 maxItemsInObjectGraph 配额的限制。

指定数据契约代理

某些 DataContractSerializer 构造函数重载具有一个 dataContractSurrogate 参数,该参数可以设置为 null。 否则,可以使用它来指定 数据协定代理项,这是实现 IDataContractSurrogate 接口的类型。 然后,可以使用接口自定义序列化和反序列化过程。 有关详细信息,请参阅 数据契约代理

序列化

以下信息适用于继承自 XmlObjectSerializer 的任何类,包括 DataContractSerializerNetDataContractSerializer 类。

简单序列化

序列化对象的最基本方法是将其 WriteObject 传递给方法。 该方法有三个重载,每个重载分别用于写入到 StreamXmlWriterXmlDictionaryWriter。 使用 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>  

分步引导的序列化

使用WriteStartObjectWriteObjectContentWriteEndObject方法分别写入结束元素、写入对象内容,并关闭包装元素。

注释

这些方法没有 Stream 重载。

此分步序列化有两个常见用途。 一个是插入属性或注释等WriteStartObjectWriteObjectContent内容,如以下示例所示。

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>  

另一个常见用途是完全避免使用 WriteStartObjectWriteEndObject,改为编写自己的自定义包装器元素(甚至可以完全跳过编写包装器),如以下代码所示。

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 的任何类,包括 DataContractSerializerNetDataContractSerializer 类。

反序列化对象的最基本方法是调用其中一个ReadObject方法重载。 该方法有三个重载,每个重载分别用于读取 XmlDictionaryReaderXmlReaderStream。 请注意, Stream 重载会创建不受任何配额保护的文本 XmlDictionaryReader ,并且只应用于读取受信任的数据。

另请注意,ReadObject 方法返回的对象必须被强制转换为适当的类型。

下面的代码构造一个实例 DataContractSerializerXmlDictionaryReader然后反序列化一个 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

DataContractSerializerNetDataContractSerializer的主要区别在于,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 序列化机制与此类似(尤其是 BinaryFormatterSoapFormatter)。

使用方法 NetDataContractSerializer 与使用 DataContractSerializer方法类似,但存在以下差异:

  • 构造函数不需要指定根类型。 可以使用NetDataContractSerializer的同一实例序列化任何类型。

  • 构造函数不接受已知类型的列表。 如果类型名称序列化为 XML,则不需要已知类型机制。

  • 构造函数不接受数据协定代理项, 相反,它们接受一个名为ISurrogateSelectorsurrogateSelector参数(该参数映射到SurrogateSelector属性)。 这是一种旧代理机制。

  • 构造函数接受 assemblyFormat 的一个名为 FormatterAssemblyStyle 的参数,该参数映射到 AssemblyFormat 属性。 如前所述,这可用于增强序列化程序的版本控制功能。 这与 FormatterAssemblyStyle 二进制或 SOAP 序列化中的机制相同。

  • 构造函数接受一个称为StreamingContextcontext参数,该参数映射到Context属性。 可以使用该参数将信息传递到要序列化的类型中。 此用法与其他 StreamingContext 类中使用的 System.Runtime.Serialization 机制相同。

  • Serialize方法和Deserialize方法是WriteObject方法和ReadObject方法的别名。 这些是为了提供一个与二进制或 SOAP 序列化更一致的编程模型而存在的。

有关这些功能的详细信息,请参阅 二进制序列化

NetDataContractSerializerDataContractSerializer 所使用的 XML 格式通常不兼容。 也就是说,尝试使用其中一个序列化程序进行序列化,并使用另一个序列化程序进行反序列化不是受支持的方案。

此外,请注意,NetDataContractSerializer 不会输出对象图中每个节点的完整 .NET Framework 类型和程序集名称。 只有在信息不明确时才输出。 也就是说,它是在根对象级别进行输出并且是针对任何多态情况。

另请参阅