語言

序列化和反序列化

Windows Communication Foundation (WCF) 包含新的串行化引擎,DataContractSerializer。 DataContractSerializer 會雙向在 .NET Framework 物件與 XML 之間轉譯。 本主題說明串行化程序的運作方式。

串行化 .NET Framework 物件時,串行化程式會了解各種不同的串行化程序設計模型,包括新的 數據合約 模型。 如需支援型別的完整清單,請參閱數據合約串行化程式 支援型別。 如需數據合約的簡介,請參閱 使用數據合約。

還原串行化 XML 時,串行化程式會使用 XmlReader 和 XmlWriter 類別。 它也支援 XmlDictionaryReader 和 XmlDictionaryWriter 類別,使其在某些情況下產生優化的 XML,例如使用 WCF 二進位 XML 格式時。

WCF 也包含隨附串行化程式,NetDataContractSerializer。 NetDataContractSerializer:

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 介面,就可以透過較舊版本從較新版本的數據合約來回傳送數據,而不會遺失。 例如,假設 Person 數據合約的第 1 版包含 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號碼,後續的使用會引用已串行化的數據。

這很重要

如果數據合約中同時存在 「id」 和 「ref」 屬性 XMLElement,則會接受 「ref」 屬性,並忽略 「id」 屬性。

請務必瞭解此模式的限制:

  • DataContractSerializer 產生的 XML,preserveObjectReferences 設定為 true 與任何其他技術並無法互通,而且只能由另一個 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>

步驟-By-Step 串行化

使用 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 屬性或建構函式參數)。 系結器只允許載入安全類型。 這個 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 屬性。 如先前所述,這可用來增強串行化程式的版本控制功能。 這與二進位或 SOAP 串行化中的 FormatterAssemblyStyle 機制相同。

  • 建構函式接受一個名為 StreamingContext 的 context 參數,該參數對應至 Context 屬性。 您可以使用這個,將資訊傳遞至要序列化的型別。 此用法與其他 StreamingContext 類別中使用的 System.Runtime.Serialization 機制相同。

  • Serialize 和 Deserialize 方法是 WriteObject 和 ReadObject 方法的別名。 這些存在以提供具有二進位或SOAP串行化的更一致程序設計模型。

如需這些功能的詳細資訊,請參閱 二進位串行化。

NetDataContractSerializer 和 DataContractSerializer 使用的 XML 格式通常不相容。 也就是說,嘗試使用其中一個串行化工具進行串行化,並使用另一個工具進行還原是未支援的情境。

此外,請注意,NetDataContractSerializer 不會輸出物件圖形中每個節點的完整 .NET Framework 類型和元件名稱。 它只會在模棱兩可的位置輸出該資訊。 也就是說,它會在根物件層級以及任何多態案例中輸出。

另請參閱