Сериализация и десериализация
Windows Communication Foundation (WCF) включает новый модуль сериализации — DataContractSerializer. Сериализатор DataContractSerializer преобразует объекты .NET Framework в формат XML и обратно. В данном разделе объясняется, как работает сериализатор.
При сериализации объектов .NET Framework сериализатор поддерживает различные модели программирования сериализации, включая новую модель контракта данных. Полный список поддерживаемых типов см. в разделе Типы, поддерживаемые сериализатором контракта данных. Вводные сведения о контрактах данных см. в разделе Использование контрактов данных.
При десериализации XML-кода сериализатор использует классы XmlReader и XmlWriter. Благодаря поддержке классов XmlDictionaryReader и XmlDictionaryWriter в некоторых случаях сериализатор может создавать оптимизированный XML-код, например при использовании двоичного XML-формата WCF.
WCF также включает сопровождающий сериализатор — NetDataContractSerializer. Сериализатор NetDataContractSerializer аналогичен сериализаторам BinaryFormatter и SoapFormatter в том, что он отображает имена типов .NET Framework как часть сериализованных данных. Он применяется при совместном использовании одних и тех же типов на концах сериализации и десериализации. Оба сериализатора (DataContractSerializer и NetDataContractSerializer) являются производными от общего базового класса XmlObjectSerializer.
Внимание! |
---|
Класс DataContractSerializer сериализует строки, содержащие управляющие символы с шестнадцатеричным значением меньше 20 в виде сущностей XML. При этом может возникнуть проблема отправки клиентами не WCF таких данных в службу WCF. |
Создание экземпляра DataContractSerializer
Создание экземпляра DataContractSerializer является важным этапом. После создания экземпляра невозможно менять какие-либо настройки.
Задание корневого типа
Корневой тип — это тип, экземпляры которого сериализуются и десериализуются. Сериализатор 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 или какого-либо другого механизма, задействован полиморфизм, конструктору сериализатора с помощью параметра 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 определяются в соответствии с именем и пространством имен контракта данных. Имена всех внутренних элементов определяются на основе имен членов данных, а их пространство имен представляет собой пространство имен контракта данных. В следующем примере задаются значения имени Name
и пространства имен Namespace
в конструкторах классов DataContractAttribute и DataMemberAttribute.
<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. (Этот метод всегда считывает один корневой объект, но в элементах данных этого объекта могут иметься другие объекты. Эти объекты, в свою очередь, могут иметь в своем составе другие объекты и т. д.) Значение по умолчанию — 65 536. Обратите внимание, что при сериализации или десериализации массивов каждая запись массива считается отдельным объектом. Также обратите внимание, что некоторые объекты могут иметь большое представление в памяти, поэтому одной этой квоты может быть недостаточно для предотвращения атак типа "отказ в обслуживании". Дополнительные сведения см. в разделе Вопросы безопасности для данных. При необходимости увеличить квоту по сравнению со значением по умолчанию следует увеличить ее как на стороне отправки (сериализации), так и на стороне получения (десериализации), так как квота действует как при считывании, так и при записи данных.
Циклы обработки
Цикл обработки совершается, когда объект десериализуется и повторно сериализуется за одну операцию. Таким образом, он перемещается от XML-кода к экземпляру объекта, а затем возвращается в поток XML.
Некоторые перегрузки конструктора DataContractSerializer имеют параметр ignoreExtensionDataObject , для которого по умолчанию задано значение false . В этом режиме по умолчанию данные можно без потерь отправлять в цикл обработки от новой версии контракта данных через старую версию обратно к новой при условии, что контракт данных реализует интерфейс IExtensibleDataObject. Предположим, например, что версия 1 контракта данных Person
содержит члены данных 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/. Каждый блок данных сериализуется только один раз, ему присваивается идентификационный номер, и при использовании этих блоков в будущем создается ссылка на уже сериализованные данные.
Примечание |
---|
Если в контракте данных XMLElement присутствует и атрибут "id", и атрибут "ref", используется атрибут "ref", а атрибут "id" игнорируется. |
Важно понимать существующие в этом режиме ограничения.
XML-код, создаваемый сериализатором DataContractSerializer с помощью preserveObjectReferences с заданным значением true, не поддерживает возможность взаимодействия с какими-либо другими технологиями, доступ к нему может осуществлять только другой экземпляр DataContractSerializer, для preserveObjectReferences которого также задано значение true.
Эта функция не обеспечивается поддержкой метаданных (схемы). Созданная схема действительна, только если preserveObjectReferences задано значение false.
Эта функция может замедлить процесс сериализации и десериализации. Несмотря на отсутствие необходимости в репликации данных, в этом режиме необходимо выполнять дополнительные сравнения объектов.
Внимание! |
---|
Если включен режим preserveObjectReferences, очень важно задать правильную квоту значению maxItemsInObjectGraph. Особенности обработки массивов в этом режиме позволяют злоумышленнику легко создавать небольшое вредоносное сообщение, которое приводит к расходованию больших объемов памяти, и препятствовать этому можно только заданием квоты maxItemsInObjectGraph. |
Задание суррогата контракта данных
Некоторые перегрузки конструктора DataContractSerializer имеют параметр dataContractSurrogate, для которого может быть задано значение null. В противном случае можно использовать этот параметр для задания суррогата контракта данных, представляющего собой тип, который реализует интерфейс IDataContractSurrogate. Затем с помощью интерфейса можно настроить процесс сериализации и десериализации. Дополнительные сведения см. в разделе Суррогаты контрактов данных.
Сериализация
Следующая информация применима к любому классу, наследуемому от сериализатора XmlObjectSerializer, включая классы DataContractSerializer и NetDataContractSerializer
Простая сериализация
Основным способом сериализации объекта является его передача методу WriteObject. Существует три перегрузки, каждая из которых предназначена для записи в Stream, XmlWriter или XmlDictionaryWriter соответственно. При использовании перегрузки Stream на выходе получается XML-код в кодировке UTF-8. При использовании перегрузки 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 в программе-оболочке или не имеющем содержимого узле, который предшествует программе-оболочке. Это можно осуществить вызовом метода Read устройства чтения XmlReader или производного метода и тестированием типа 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 выводит полную сборку .NET Framework и имена типов в сериализованном XML-коде. Это означает, что одни и те же типы должны совместно использоваться конечными точками сериализации и десериализации. Так как при использовании сериализатора NetDataContractSerializer всегда известны точные типы, которые должны быть десериализованы, механизм известных типов не требуется.
Однако в связи с этим могут возникнуть некоторые проблемы:
Безопасность. Загружается любой тип, обнаруживаемый в десериализуемом XML-коде. Этим можно воспользоваться для принудительной загрузки вредоносных типов. Использование сериализатора NetDataContractSerializer с ненадежными данными возможно только при условии применения связывателя сериализации (с использованием свойства Binder или параметра конструктора). Связыватель позволяет загружать только надежные типы. Механизм связывателя аналогичен используемому типами в пространстве имен System.Runtime.Serialization.
Управление версиями. Использование полных имен типов и сборок в XML-коде строго ограничивает возможности управления версиями типов. При этом невозможно изменить имена типов, пространства имен, имена и версии сборок. Задание свойству AssemblyFormat или параметру конструктора значения Simple вместо значения по умолчанию Full позволяет изменить версию сборки, но не универсальные типы параметров.
Взаимодействие. Так как имена типов и сборок .NET Framework включены в XML-код, никакие другие платформы, кроме .NET Framework, не могут получить доступ к получаемым данным.
Производительность. Запись имен типов и сборок значительно увеличивает размер получаемого XML-кода.
Этот механизм аналогичен двоичной сериализации или сериализации SOAP, используемой удаленным взаимодействием .NET Framework (а именно, BinaryFormatter и SoapFormatter).
Сериализаторы NetDataContractSerializer и DataContractSerializer используются практически аналогично со следующими отличиями.
Конструкторы не требуют указания корневого типа. Любой тип можно сериализовать с помощью одного и того же экземпляра сериализатора NetDataContractSerializer.
Конструкторы не принимают список известных типов. Если имена типов сериализуются в XML-код, механизм известных типов не требуется.
Конструкторы не принимают суррогаты контракта данных. Вместо этого они принимают параметр ISurrogateSelector, называемый surrogateSelector (который сопоставляется со свойством SurrogateSelector). Это устаревший суррогатный механизм.
Конструкторы принимают параметр стиля FormatterAssemblyStyle, называемый assemblyFormat, который сопоставляется со свойством AssemblyFormat. Как уже отмечалось ранее, эту особенность можно использовать для расширения возможностей сериализатора по управлению версиями. Этот подход аналогичен механизму FormatterAssemblyStyle в двоичной сериализации или сериализации SOAP.
Конструкторы принимают параметр StreamingContext, называемый context, который сопоставляется со свойством Context. Эту особенность можно использовать для передачи информации в сериализуемые типы. Данный подход аналогичен использованию механизма StreamingContext в других классах System.Runtime.Serialization.
Методы Serialize и Deserialize представляют собой псевдонимы для методов WriteObject и ReadObject. Они предоставляют более последовательную модель программирования с двоичной сериализацией или сериализацией SOAP.
Дополнительные сведения этих функциях см. в разделе Binary Serialization.
Форматы XML, используемые сериализаторами NetDataContractSerializer и DataContractSerializer, как правило, не совместимы. Следовательно, возможность сериализации с помощью одного из них и десериализации с помощью другого не поддерживается.
Обратите также внимание, что сериализатор NetDataContractSerializer не выдает полное имя типа и сборки .NET Framework для каждого узла в графе объекта. Он выводит эту информацию, только если она неоднозначна. Таким образом, вывод данных осуществляется на уровне корневого объекта и для любых полиморфных случаев.
См. также
Справочник
DataContractSerializer
NetDataContractSerializer
XmlObjectSerializer
Основные понятия
Типы, поддерживаемые сериализатором контракта данных