序列化和還原序列化
Windows Communication Foundation (WCF) 包含新的序列化 (Serialization) 引擎,DataContractSerializer。 DataContractSerializer 會在 .NET Framework 物件與 XML 之間轉譯 (雙向)。 本主題會說明序列化程式的運作方式。
序列化 .NET Framework 物件時,序列化程式了解各種不同的序列化程式設計模型,包括新的「資料合約」(Data Contract) 模型。 如需完整的支援型別清單,請參閱資料合約序列化程式支援的型別。 如需資料合約的簡介,請參閱使用資料合約。
還原序列化 XML 時,序列化程式會使用 XmlReader 和 XmlWriter 類別。 它同時支援 XmlDictionaryReader 和 XmlDictionaryWriter 類別,以便讓它在某些情況下產生最佳化 XML,例如當使用 WCF 二進位 XML 格式時。
WCF 同時包含搭配的序列化程式,NetDataContractSerializer。 NetDataContractSerializer 與 BinaryFormatter 和 SoapFormatter 序列化程式很類似,因為它同時會發出 .NET Framework 型別名稱做為序列化資料的一部分。 當序列化和還原序列化兩端共用相同的型別時,就會使用它。 DataContractSerializer 和 NetDataContractSerializer 兩者同時衍生自一般基底類別,XmlObjectSerializer。
注意: |
---|
DataContractSerializer 會將包含十六進位值低於 20 之控制字元的字串序列化成 XML 實體。 當非 WCF 用戶端將這類資料傳送至 WCF 服務時,這可能會導致問題。 |
建立 DataContractSerializer 執行個體
建構 DataContractSerializer 的執行個體是很重要的步驟。 建構完成之後,您將無法變更任何設定。
指定根型別
「根型別」(Root Type) 是指已序列化或還原序列化之執行個體的型別。 DataContractSerializer 包含許多建構函式多載,但是,根型別至少必須透過 type 參數來提供。
針對特定根型別所建立的序列化程式無法用來序列化 (或還原序列化) 另一種型別,除非該型別衍生自根型別。 以下範例顯示兩個類別。
<DataContract()> _
Public Class Person
' Code not shown.
End Class
<DataContract()> _
Public Class PurchaseOrder
' Code not shown.
End Class
[DataContract]
public class Person
{
// Code not shown.
}
[DataContract]
public class PurchaseOrder
{
// Code not shown.
}
這個程式碼所建構的 DataContractSerializer 執行個體只能用來序列化或還原序列化 Person
類別的執行個體。
Dim dcs As New DataContractSerializer(GetType(Person))
'This can now be used to serialize/deserialize Person but not PurchaseOrder.
DataContractSerializer dcs = new DataContractSerializer(typeof(Person));
//This can now be used to serialize/deserialize Person but not PurchaseOrder.
指定已知型別
如果透過 KnownTypeAttribute 屬性或其他一些機制來序列化尚未處理的型別時運用了多型 (Polymorphism),則必須透過 knownTypes 參數將可能的已知型別清單傳遞至序列化程式的建構函式中。如需詳細資訊已知型別的詳細資訊,請參閱資料合約已知型別。
下列範例說明類別 (LibraryPatron
),其中包含了特定型別的集合 (LibraryItem
)。 第二個類別定義了 LibraryItem
型別。 第三與第四個類別 (Book
和 Newspaper
) 則繼承了 LibraryItem
類別。
<DataContract()> _
Public Class LibraryPatron
<DataMember()> _
Public borrowedItems() As LibraryItem
End Class
<DataContract()> _
Public Class LibraryItem
'code not shown
End Class 'LibraryItem
<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.
Dim knownTypes() As Type = {GetType(Book), GetType(Newspaper)}
Dim dcs As New DataContractSerializer(GetType(LibraryPatron), knownTypes)
' All types are known after construction.
//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.
指定預設的根名稱與命名空間
一般來說,在序列化物件時,最外部 XML 項目的預設名稱與命名空間會依據資料合約名稱與命名空間來決定。 所有內部項目的名稱都會從資料成員名稱中決定,而其命名空間則是資料合約的命名空間。 下列範例會在 DataContractAttribute 和 DataMemberAttribute 類別的建構函式中設定 Name
和 Namespace
值。
<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
[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;
}
序列化 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。 請注意,當序列化或還原序列化陣列時,每個陣列項目都視為個別物件。 另外請注意,有些物件可能有大量記憶體表示,因此只有這個配額可能不足以防止阻絕服務攻擊。 如需詳細資訊,請參閱 資料的安全性考量. 如果您需要將這個配額調高到預設值以上,請在傳送 (序列化) 和接收 (還原序列化) 端都執行這項操作,因為它會在讀取與寫入資料時同時套用。
來回行程
在一個作業中還原序列化物件後重新加以序列化的程序,我們稱為「來回行程」(Round Trip)。 因此,它會從 XML 變成物件執行個體,並回到原本的 XML 資料流。
某些 DataContractSerializer 建構函式多載包含有 ignoreExtensionDataObject 參數 (預設為 false )。 在此預設模式中,只要資料合約實作 IExtensibleDataObject 介面,資料就可以從較新版的資料合約一路經由較舊版本的資料合約再傳回較新版的資料合約來完成無資料損失的來回行程。 例如,假定 Person
資料合約的第 1 版包含 Name
和 PhoneNumber
資料成員,而第 2 版新增了 Nickname
成員。 如果實作了 IExtensibleDataObject,則當從第 2 版傳送資料到第 1 版時,會儲存 Nickname
資料,然後在資料序列化期間重新將它發出,這樣一來,來回行程期間就不會損失任何資料。如需詳細資訊,請參閱向前相容資料合約和資料合約版本控制。
來回行程的安全性與結構描述有效性考量
來回行程可能包含一些安全性意涵。 例如,還原序列化與儲存大量的非直接關聯資料時,可能會帶來安全性風險。 由於重新發出這項資料時並無法加以驗證,特別是當牽涉到數位簽章時,因此可能會有一些安全上的考量。 例如,在之前的案例中,第 1 版的端點可能會簽署包含惡意資料的 Nickname
值。 最後,還是可能有一些結構描述上的有效性考量:端點可能想要一直發出能夠恪遵其陳述的合約規定,而非其他額外值的資料。 在先前的範例中,第 1 版端點的合約告知僅發出 Name
和 PhoneNumber
,而且假如使用結構描述驗證法的話,發出額外的 Nickname
值將讓驗證失敗。
啟用和停用來回行程
若要關閉來回行程,請勿實作 IExtensibleDataObject 介面。 如果您無法掌控型別,請將 ignoreExtensionDataObject 參數設定為 true 來達到相同的效果。
物件圖形保留
一般來說,序列化程式並不關心物件身分識別,如下列程式碼所示。
<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
[DataContract]
public class PurchaseOrder
{
[DataMember]
public Address billTo;
[DataMember]
public Address shipTo;
}
[DataContract]
public class Address
{
[DataMember]
public string street;
}
下列程式碼會建立採購單。
'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
//Construct a purchase order:
Address adr = new Address();
adr.street = "123 Main St.";
PurchaseOrder po = 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" 命名空間指的是標準序列化命名空間 https://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)。 另一方面,您可以用它來指定「資料合約代理」(Data Contract Surrogate),這是一種可實作 IDataContractSurrogate 介面的型別。 您可以接著使用此介面來自訂序列化與還原序列化處理序。 如需詳細資訊,請參閱 資料合約代理.
序列化
下列資訊將套用至任何繼承自 XmlObjectSerializer 的類別,包括 DataContractSerializer 和 NetDataContractSerializer 類別。
簡單序列化
序列化物件的最基本方式,就是將它傳遞給 WriteObject 方法。 有三種多載,各自負責寫入至 Stream、XmlWriter,或是 XmlDictionaryWriter。 在使用 Stream 多載的情況下,將輸出 UTF-8 編碼格式的 XML。 在使用 XmlDictionaryWriter 多載的情況下,序列化程式會最佳化二進位 XML 的輸出。
使用 WriteObject 方法時,序列化程式會使用包裝函式項目的預設名稱與命名空間,並隨著內容一起寫出 (請參閱先前的「指定預設的根名稱與命名空間」一節說明)。
下列範例示範使用 XmlDictionaryWriter 來寫入。
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)
Person p = new Person();
DataContractSerializer dcs =
new DataContractSerializer(typeof(Person));
XmlDictionaryWriter xdw =
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
執行個體。
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)
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);
在呼叫 ReadObject 方法之前,請將 XML 讀取器置於包裝函式項目或是位於包裝函式項目之前的非內容節點上。 您可以呼叫 XmlReader 或其衍生的 Read 方法,然後測試 NodeType 來達到這個目的,如下列程式碼所示。
Dim ser As New DataContractSerializer(GetType(Person), "Customer", "https://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
DataContractSerializer ser = new DataContractSerializer(typeof(Person),
"Customer", @"https://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("{0} {1} id:{2}",
p.Name , p.Address);
}
Console.WriteLine(reader.Name);
break;
}
}
請注意,在將讀取器交給 ReadObject
之前,可以讀取此包裝函式項目的屬性。
在使用其中一個簡單 ReadObject
多載之前,還原序列化程式會在包裝函式項目上尋找預設名稱與命名空間 (請參閱上一節「指定預設的根名稱與命名空間」) 並在它找到未知項目時擲回例外狀況。 在先前的範例中,預期會使用 <Person>
包裝函式項目。 IsStartObject 方法會被呼叫,以驗證讀取器位於如預期命名的項目上。
有一種方式可以停用此包裝函式項目名稱檢查;某些 ReadObject 方法的多載會採用布林參數 verifyObjectName (預設會設定為 true)。 一旦設定為 false,就會忽略包裝函式項目的名稱與命名空間。 讀取以先前所述之逐步序列化機制撰寫的 XML 時,這種方式很有用。
使用 NetDataContractSerializer
DataContractSerializer 和 NetDataContractSerializer 之間的最主要差異,就是 DataContractSerializer 使用資料合約名稱,而 NetDataContractSerializer 則是在序列化 XML 中輸出完整的 .NET Framework 組件和型別名稱。 也就是說,序列化與還原序列化端點兩者必須共用完全相同的型別。 這表示 NetDataContractSerializer 並不需要已知型別機制,因為要還原序列化的完全相同型別一律呈現已知狀態。
但是,還是會發生一些問題:
安全性。 在還原序列化的 XML 中找到的任何型別會被載入。 攻擊者會利用這個漏洞來強制載入惡意型別。 只有在使用「序列化繫結器」(Serialization Binder) 時,才應該針對不受信任的資料使用 NetDataContractSerializer (透過 Binder 屬性或建構函式參數)。 繫結器只允許載入安全型別。 繫結器機制與 System.Runtime.Serialization 命名空間用途中的型別相同。
版本控制。 在 XML 中使用完整的型別與組件名稱會嚴格限制型別的版本設定。 下列為無法變更的項目:型別名稱、命名空間、組件名稱以及組件版本。 將 AssemblyFormat 屬性或建構函式設定為 Simple (而不是 Full 的預設值) 可允許組件版本變更,但不適用於泛型參數型別。
互通性。 由於 XML 包含了 .NET Framework 型別和組件名稱,.NET Framework 以外的平台將無法存取最後資料。
效能。 寫出型別和組件名稱會大幅增加最後 XML 的大小。
這項機制與 .NET Framework 遠端處理 (特別是 BinaryFormatter 和 SoapFormatter) 所使用的二進位或 SOAP 序列化很類似。
使用 NetDataContractSerializer 類似於使用 DataContractSerializer,除了下列幾點差異以外:
建構函式不會要求您指定根型別。 您可以使用相同的 NetDataContractSerializer 執行個體來序列化任何型別。
建構函式不接收已知型別清單。 如果型別名稱已經序列化為 XML,就不需要建構函式機制。
建構函式不接受資料合約代理。 反之,它們接受名為 surrogateSelector (對應至 SurrogateSelector 屬性) 的 ISurrogateSelector 參數。 這是一項傳統的代理機制。
建構函式接受 FormatterAssemblyStyle 中 (對應至 AssemblyFormat 屬性) 名為 assemblyFormat 的參數。 如先前所述,它可用來加強序列化程式的版本控制功能。 這與二進位或 SOAP 序列化中的 FormatterAssemblyStyle 機制相同。
建構函式接受稱為 context (對應至 Context 屬性) 的 StreamingContext 參數。 您可以透過這項參數將資訊傳遞至正要序列化的型別中。 這項用法與其他 System.Runtime.Serialization 類別使用的 StreamingContext 機制用法相同。
Serialize 和 Deserialize 方法都是 WriteObject 和 ReadObject 方法的別名。 這些都是為了在使用二進位或 SOAP 序列化時提供更一致的程式設計模型而存在的。
如需詳細資訊這些功能的詳細資訊,請參閱Binary Serialization。
NetDataContractSerializer 和 DataContractSerializer 使用的 XML 格式一般都不相容。 亦即,不支援使用其中一個序列化程式來序列化,並以另一個序列化程式來還原序列化的情況。
同時請注意,NetDataContractSerializer 不會在物件圖形中為每個節點輸出完整的 .NET Framework 型別和組件名稱。 它只會針對不夠清楚的部分來輸出資訊。 亦即,它會在根物件層級以及任何多型案例中輸出。
另請參閱
參考
DataContractSerializer
NetDataContractSerializer
XmlObjectSerializer