Поделиться через


Сериализация и десериализация

Windows Communication Foundation (WCF) включает новый модуль сериализации, DataContractSerializer. DataContractSerializer выполняет преобразование между объектами .NET Framework и XML в обе стороны. В этом разделе объясняется, как работает сериализатор.

При сериализации объектов .NET Framework сериализатор понимает различные модели программирования сериализации, включая новую модель контракта данных. Для получения полного списка поддерживаемых типов см. Типы, поддерживаемые Сериализатором контракта данных. Общие сведения о контрактах данных см. в разделе Использование контрактов данных.

При десериализации XML сериализатор использует классы XmlReader и XmlWriter. Он также поддерживает классы XmlDictionaryReader и XmlDictionaryWriter, чтобы он мог создавать оптимизированный XML в некоторых случаях, например при использовании двоичного xml-формата WCF.

WCF также включает сопутствующий сериализатор, NetDataContractSerializer. NetDataContractSerializer:

  • не безопасно? Дополнительные сведения см. в статье Руководство по безопасности BinaryFormatter.
  • Аналогичен сериализаторам BinaryFormatter и SoapFormatter, так как он также выдает имена типов .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. Обратите внимание, что при сериализации или десериализации массивов каждая запись массива считается отдельным объектом. Кроме того, обратите внимание, что некоторые объекты могут занимать большой объем памяти, поэтому эта квота может быть недостаточной для предотвращения атаки типа "Denial of Service". Дополнительные сведения см. в разделе Вопросы безопасности данных для. Если необходимо увеличить эту квоту за пределы значения по умолчанию, важно сделать это как для отправки (сериализации), так и для получения (десериализации) сторон, так как она применяется как к чтению, так и к записи данных.

Поездки туда и обратно

круговой процесс происходит, когда объект десериализуется и затем снова сериализуется в рамках одной операции. Таким образом, он переходит из 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" и "ref" присутствуют в XMLElementконтракта данных, то атрибут "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.

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 выводит полные имена сборок и типов .NET Framework в сериализованном XML. Это означает, что одинаковые типы должны использоваться между конечными точками сериализации и десериализации. Это означает, что механизм известных типов не требуется для NetDataContractSerializer, так как точные типы, которые необходимо десериализировать, всегда известны.

Однако могут возникнуть некоторые проблемы:

  • Безопасность. Загружается любой тип, обнаруженный в процессе десериализации XML. Это можно использовать для принудительной загрузки вредоносных типов. Использовать NetDataContractSerializer с ненадежными данными следует только в том случае, если применяется привязка сериализации (используя свойство или параметр конструктора Binder). Привязыватель позволяет загружать только безопасные типы. Механизм 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). Это устаревший суррогатный механизм.

  • Конструкторы принимают параметр, называемый assemblyFormat, из FormatterAssemblyStyle, который сопоставляется со свойством AssemblyFormat. Как упоминалось ранее, это можно использовать для улучшения возможностей управления версиями сериализатора. Это идентично механизму FormatterAssemblyStyle при бинарной или в формате SOAP сериализации.

  • Конструкторы принимают параметр StreamingContext с именем context, который сопоставляется со свойством Context. Это можно использовать для передачи информации в типы, которые сериализуются. Это использование идентично использованию механизма StreamingContext, используемого в других классах System.Runtime.Serialization.

  • Методы Serialize и Deserialize являются псевдонимами для методов WriteObject и ReadObject. Они существуют для обеспечения более согласованной модели программирования с использованием как двоичной, так и сериализации SOAP.

Дополнительные сведения об этих функциях см. в двоичной сериализации.

Форматы XML, которые используются NetDataContractSerializer и DataContractSerializer, обычно несовместимы. То есть попытка сериализировать с одним из этих сериализаторов и десериализировать с другим — это неподдерживаемая ситуация.

Кроме того, обратите внимание, что NetDataContractSerializer не выводит полный тип .NET Framework и имя сборки для каждого узла в графе объектов. Он выводит информацию только в том месте, где она неоднозначна. То есть он выводит данные на уровне корневого объекта и для любых полиморфных случаев.

См. также