Xml сериализация в .Net Framework 2.0
ОГЛАВЛЕНИЕ
Как работает Xml сериализация
При Xml сериализации, сериализуются все открытые поля и свойства класса. Кроме того, открытые свойства должны иметь аксессоры get и set, а сам класс должен иметь конструктор по умолчанию без параметров. Рассмотрим класс:public class DataClassЕго мы будем использовать во всех дальнейших примерах сериализации и десериализации.
{
public DataClass(){}
public string ID = Guid.NewGuid().ToString();
public string Name = "Just Name";
public Decimal Count = 10;
public DateTime Date = DateTime.Now;
}
Для сериализации нам надо создать экземпляр класса XmlSerializer, предав в качестве параметра конструктора тип сериализуемого класса. Следующий код демонстрирует сериализацию объекта нашего класса DataClass:
DataClass obj = new DataClass();
// создаем сериалайзер
XmlSerializer sr = new XmlSerializer(obj.GetType());
// создаем writer, в который будет происходить сериализация
StringBuilder sb = new StringBuilder();
StringWriter w = new StringWriter(sb, System.Globalization.CultureInfo.InvariantCulture);
// сериализуем
sr.Serialize(w,obj);
// получаем строку Xml
string xml = sb.ToString();
Console.WriteLine(xml);
В результате получаем вот такой Xml:
<?xml version="1.0" encoding="utf-8"?>
<DataClass xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<ID>34332413-e70f-44f7-b35c-b34c47812dbc</ID>
<Name>Just Name</Name>
<Count>10</Count>
<Date>2006-10-30T12:35:20.3110319+03:00</Date>
</DataClass>
Для того, чтобы получить из этого Xml экземпляр класса DataClass (десериализовать), служит следующий код:
// создаем readerУправлять, тем как свойства и поля объекта отображаются на элементы Xml, можно при помощи атрибутов, которые содержатся в пространстве имен System.Xml.Serialization. Допустим, мы хотим, чтобы в результирующем Xml корневой элемент назывался <Data>, поля “ID”, “Name” сериализовались Xml атрибутами, поле “Count” соответствовало Xml элементу <Reserved>, а поле “Date”, вообще не попадало в результирующий Xml. Для этого расставим соответствующие атрибуты:
StringReader reader = new StringReader(xml);
// создаем XmlSerializer
XmlSerializer dsr = new XmlSerializer(typeof(DataClass));
// десериализуем
DataClass clone = (DataClass)dsr.Deserialize(reader);
[XmlRoot("Data")] // изменим имя корневого элементаРезультирующий Xml теперь выглядит так:
public class DataClass
{
public DataClass(){}
[XmlAttribute] // сериализуем в xml атрибут
public string ID = Guid.NewGuid().ToString();
[XmlAttribute] // сериализуем в xml атрибут
public string Name = "Just Name";
[XmlElement("Reserved")] // изменим имя xml элемента
public Decimal Count = 10;
[XmlIgnore] // не будет сериализоваться
public DateTime Date = DateTime.Now;
}
<?xml version="1.0" encoding="utf-8"?>Как видно из представленного кода, использовать Xml сериализацию не сложно. Но все же попробуем облегчить себе жизнь. Для этого вынесем код, ответственный за сериализацию в отдельный класс утилиту:
<Data xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
ID="cc340d5e-4c80-4fe5-9c3d-e49f64e26b22" Name="Just Name">
<Reserved>10</Reserved>
</Data>
public class XmlUtilityНазначение метода Obj2XmlStr(object obj, string nameSpace) я объясню несколько позже. Теперь наш код для сериализации и десериализации объекта сократился в несколько раз:
{
/// <summary>
/// Сериализует объект в строку XML
/// </summary>
public static string Obj2XmlStr(object obj, string nameSpace)
{
if (obj == null)
return string.Empty;
XmlSerializer sr = new XmlSerializer(type);
StringBuilder sb = new StringBuilder();
StringWriter w = new StringWriter(sb, System.Globalization.CultureInfo.InvariantCulture);
sr.Serialize(
w,
obj,
new XmlSerializerNamespaces(
new XmlQualifiedName[]
{
new XmlQualifiedName("", nameSpace)
}
));
return sb.ToString();
}
/// <summary>
/// Сериализует объект в строку XML
/// </summary>
public static string Obj2XmlStr(object obj)
{
if (obj == null) return string.Empty;
XmlSerializer sr = new XmlSerializer(obj.GetType());
StringBuilder sb = new StringBuilder();
StringWriter w = new StringWriter(sb, System.Globalization.CultureInfo.InvariantCulture);
sr.Serialize(
w,
obj,
new XmlSerializerNamespaces( new XmlQualifiedName[]
{ new XmlQualifiedName(string.Empty) } ) );
return sb.ToString();
}
/// <summary>
/// Десериализует строку XML в объект заданного типа
/// </summary>
/// <param name="xml"></param>
/// <param name="type"></param>
/// <returns></returns>
public static T XmlStr2Obj<T>(string xml)
{
if (xml == null) return default(T);
if (xml == string.Empty) return (T)Activator.CreateInstance(typeof(T));
StringReader reader = new StringReader(xml);
XmlSerializer sr = new XmlSerializer(typeof(T));//SerializerCache.GetSerializer(type);
return (T)sr.Deserialize(reader);
}
}
DataClass obj = new DataClass();
string xml = XmlUtility.Obj2XmlStr(obj);
Console.WriteLine(xml);
DataClass clone = XmlUtility.XmlStr2Obj<DataClass>(xml);
Полный код класса XmlUtility вы найдете в исходном коде, прилагаемом к статье.
Namespce и сериализация
Когда мы используем для сериализации метод XmlUtility.Obj2XmlStr(obj), мы можем заметить, что результирующий xml получается несколько более компактным чем прежде потому, что из него исчезли стандартные префиксы нэймспейсов XmlSchema - xsd и XmlSchema Instance - xsi :<?xml version="1.0" encoding="utf-8"?>Произошло это из-за использования перегруженного метода SmlSerializer.Serialize(), в котором явно указывается перечень используемых Xml namespace – ов (пустой в данном конкретном случае).
<Data ID="078ce191-f781-4052-93fd-766309c2abaa" Name="Just Name">
<Reserved>10</Reserved>
</Data>
Есть в нашей утилите и метод, позволяющий явно задать namespace для сериализации - Obj2XmlStr(object obj, string nameSpace). Учет Xml namespace при сериализации очень важен. Рассмотрим следующий пример:
DataClass obj = new DataClass();При попытке десериализации полученного Xml обратно в объект, в пятой строке мы получим исключение:
// зададим явно XmlNamespace при сериализации
string xml = XmlUtility.Obj2XmlStr(obj, "urn:MyDataClass");
Console.WriteLine(xml);
DataClass clone = XmlUtility.XmlStr2Obj<DataClass>(xml);
<?xml version="1.0" encoding="utf-8"?>Дело в том, что сериалайзеру не было никаких указаний по поводу Xml namespace для данного класса. И появление объявления namespace по умолчанию xmlns="urn:MyDataClass" становится непреодолимой преградой на пути десериализации Xml в объект. Явно указать, какой namespace надо использовать сериалайзеру можно в атрибуте XmlRootAttribute. Например:
<Data xmlns="urn:MyDataClass" ID="e46917f4-691a-4a7f-9f0b-d9be5e018a1f" Name="Just Name">
<Reserved>10</Reserved>
</Data>
System.InvalidOperationException : <Data xmlns='urn:MyDataClass'> was not expected.
[XmlRoot("Data", Namespace=DataClass.XmlNamespace )]После этих изменений в коде, ошибка времени выполнения при десериализации пропадает и код, приведенный ранее, выполняется.
public class DataClass
{
public const string XmlNamespace = "urn:MyDataClass";
А что будет, если теперь при сериализации не указывать namespace явно, а вызвать
string xml = XmlUtility.Obj2XmlStr(obj);Результирующий Xml опять изменится, но сериализация и десериализация выполнятся без ошибок:
<?xml version="1.0" encoding="utf-8"?>
<q1:Data ID="1458c25e-4356-4df5-b918-a44c9bf5e4b0" Name="Just Name" xmlns:q1="urn:MyDataClass">
<q1:Reserved>10</q1:Reserved>
</q1:Data>
Мы видим, что все xml элементы получили префикс “ql”. Произошло это из-за того, что namespace "urn:MyDataClass" объявлен в атрибуте XmlRoot нашего класса, а при сериализации он не был задан в качестве namespace по умолчанию. Таких ситуаций следует избегать. Лучше, либо вообще не использовать namespace, либо задавать их явно в качестве namespace по умолчанию, что позволяет избежать использования префиксов в Xml. Nmespace можно задать не только для класса, но и для каждого элемента в отдельности (но без нужды лучше этого не делать). Кроме того, при разработке крупных систем, может оказаться, что для разных классов заданы разные namespace, и тут уже не избежать появления префиксов имен в результирующем Xml. Поэтому данному вопросу необходимо постоянно уделять внимание.
Сериализация массивов и коллекций
Рассмотрим некоторые особенности сериализации массивов и коллекций. Для этого добавим в объявление нашего сериализуемого класса поле в виде массива строк:[XmlRoot("Data", Namespace=DataClass.XmlNamespace )]После сериализации мы получим следующий Xml
public class DataClass
{
...
//массив строк
public string[] Lines = new string[] { "Line one", "Line two", "Line three" };
}
<?xml version="1.0" encoding="utf-8"?>По умолчанию, массив (коллекция) сериализуется в xml элемент с именем поля или свойства, в который вложены элементы, входящие в массив. Возможно, это не совсем тот Xml который вы хотели бы увидеть. Управлять сериализацией массива помогают атрибуты XmlArray, XmlArrayItem, и XmlElement. Давайте рассмотрим эти возможности подробнее. Меня, например, не устраивает то, что элементы массива называются <string>. Мне нужно, чтобы элементы назывались <Line>. Достигается это при помощи атрибута XmlArrayItem над полем Lines. Если же нам надо изменить название элемента <Lines>, то для этого используем атрибут XmlArray:
<Data ID="cafaafbf-fa51-47d5-a921-0ffeb8a7e345" Name="Just Name" xmlns="urn:MyDataClass">
<Reserved>10</Reserved>
<Lines>
<string>Line one</string>
<string>Line two</string>
<string>Line three</string>
</Lines>
</Data>
[XmlArray("Specification")]Результирующий Xml изменился:
[XmlArrayItem("Line")]
public string[] Lines = new string[] { "Line one", "Line two", "Line three" };
<?xml version="1.0" encoding="utf-8"?>Ну, и, наконец, часто бывают ситуации, когда мы хотели бы вообще избавиться от элемента <Specification> и получить вот такой xml:
<Data ID="64a2c310-fa58-4a28-9cb2-7652625b28fc" Name="Just Name" xmlns="urn:MyDataClass">
<Reserved>10</Reserved>
<Specification>
<Line>Line one</Line>
<Line>Line two</Line>
<Line>Line three</Line>
</Specification>
</Data>
<?xml version="1.0" encoding="utf-8"?>Делается это достаточно просто. Вместо атрибутов XmlArray и XmlArrayItem следует явно задать атрибут XmlElement над полем или свойством типа массива или коллекции. Для получения приведенного выше Xml, используем следующий код:
<Data ID="58f7271e-d6b2-4c7f-b3f3-334d8f087f09" Name="Just Name" xmlns="urn:MyDataClass">
<Reserved>10</Reserved>
<Line>Line one</Line>
<Line>Line two</Line>
<Line>Line three</Line>
</Data>
[XmlElement("Line")]Подобно массивам сериализуются коллекции. Например мы можем добавить в объявление DataClass поле типа ArrayList и заполнить его различными значениями:
public string[] Lines = new string[] { "Line one", "Line two", "Line three" };
public class DataClassСериалайзер справился с поставленной задачей и выдал вот такой xml:
{
...
// коллекция
public ArrayList List = new ArrayList();
}
// создаем экземпляр и наполняем коллекцию List различными объектами
DataClass obj = new DataClass();
obj.List.Add(new DataClass());
obj.List.Add("This is a string");
string xml = XmlUtility.Obj2XmlStr(obj, DataClass.XmlNamespace);
Console.WriteLine(xml);
<?xml version="1.0" encoding="utf-8"?>Элементы списка сериализуются в xml элементы <anyType>, в которые добавляется дополнительная информация о типе каждого элемента. Однако, подобная сериализация, это не самая лучшая идея, поверьте. Особенно если данный Xml предназначен для обмена данными с другой системой. Дело в том, что в нашем случае схема результирующего Xml расширяется в runtime в зависимости от типа элементов, добавляемых в коллекцию. Далеко не все системы смогут правильно обработать такой Xml. По возможности, следует избегать использования не типизированных коллекций. Совсем другое дело - использование Generic коллекций. Модифицируем наш DataClass следующим образом, вместо ArrayList используем List<DataClass>:
<Data ID="b1d70646-0985-424a-b87b-1e910173c9e5" Name="Just Name" xmlns="urn:MyDataClass">
<Reserved>10</Reserved>
<Line>Line one</Line>
<Line>Line two</Line>
<Line>Line three</Line>
<List>
<anyType d3p1:type="DataClass"
ID="0a57f292-7a0e-479d-93e2-6c42fa019025"
Name="Just Name" xmlns:d3p1="http://www.w3.org/2001/XMLSchema-instance">
<Reserved>10</Reserved>
<Line>Line one</Line>
<Line>Line two</Line>
<Line>Line three</Line>
<List />
</anyType>
<anyType xmlns:q1="http://www.w3.org/2001/XMLSchema"
d3p1:type="q1:string"
xmlns:d3p1="http://www.w3.org/2001/XMLSchema-instance">This is a string</anyType>
</List>
</Data>
public class DataClassРезультирующий Xml выглядит теперь вот так:
{
...
//public ArrayList List = new ArrayList();
public List<DataClass> List = new List<DataClass>();
}
<?xml version="1.0" encoding="utf-8"?>
<Data ID="13688ff2-c55f-45d7-8498-750a3e5df1d1" Name="Just Name" xmlns="urn:MyDataClass">
<Reserved>10</Reserved>
<Line>Line one</Line>
<Line>Line two</Line>
<Line>Line three</Line>
<List>
<DataClass ID="c250a091-4d99-40eb-a870-201f6130579b" Name="Just Name">
<Reserved>10</Reserved>
<Line>Line one</Line>
<Line>Line two</Line>
<Line>Line three</Line>
<List />
</DataClass>
</List>
</Data>
Как мы можем убедиться из xml исчезли элементы <anyType>, потому что тип элементов коллекции известен заранее.
Xml Сериализация и наследование
У Xml сериализации непростые отношения с объектным наследованием. Рассмотрим пример. Объявим класс ChildClass – наследник DataClass, а в самом DataClass объявим поле Child:public class DataClassА теперь попробуем воспользоваться полиморфизмом, и присвоим DataClass.Child объект типа ChildClass
{
...
[XmlElement]
public DataClass Child;
...
}
public class ChildClass : DataClass
{
public string ParentName;
}
DataClass obj = new DataClass();Результат будет плачевный. XmlSerializer не приветствует наши опыты с полиморфизмом и отказывается сериализовать наши объекты, выдавая при этом исключение:
obj.Child = new ChildClass();
string xml = XmlUtility.Obj2XmlStr(obj, DataClass.XmlNamespace);
Console.WriteLine(xml);
UnitTest.XmlUtilityTest.SerializeTest : System.InvalidOperationException : There was an error generating the XML document.
----> System.InvalidOperationException : The type UnitTest.ChildClass was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically.
[XmlRoot("Data", Namespace=DataClass.XmlNamespace )]
[XmlInclude(typeof(ChildClass))]
public class DataClass
{
public const string XmlNamespace = "urn:MyDataClass";
public DataClass(){}
[XmlElement]
public DataClass Child;
...
}
[XmlType(Namespace=DataClass.XmlNamespace)]
public class ChildClass : DataClass
{
public string ParentName;
}
Причем при появлении новых наследников, мы должны добавлять новые XmlInclude над базовым классом. Если же исходный код базового класса нам не доступен, то мы попадаем в довольно сложную ситуацию. Предположим мы не можем изменять описание базового класса DataClass, потому что у нас нет доступа к его исходному коду. У нас объявлен еще один его наследник CrandChildClass:DataClass, и мы присваиваем полю DataClass.Child экземпляр этого нового типа. Сериализация в этом случае не возможна. Но выход, все-таки есть, хотя и довольно запутанный. Специально для этого случая существует тип XmlAttributeOverrides и конструктор в XmlSerializer, принимающий этот тип. Суть его использования состоит в том, что с его помощью мы в runtime можем переопределить атрибуты, управляющие xml сериализацией, заданные в исходном коде.
Вот пример:
DataClass obj = new DataClass();
// присваиваем значение наследника для которого не был определен XmlInclude
obj.Child = new GrandChildClass();
// переопределяем атрибуты сериализации
XmlAttributes attrs = new XmlAttributes();
XmlElementAttribute attr = new XmlElementAttribute();
attr.ElementName = "GrandChildClass";
attr.Type = typeof(GrandChildClass);
attrs.XmlElements.Add(attr);
XmlAttributeOverrides attrOverrides = new XmlAttributeOverrides();
attrOverrides.Add(typeof(DataClass), "Child", attrs);
// используем специальный конструктор
XmlSerializer sr = new XmlSerializer(typeof(DataClass), attrOverrides);
StringBuilder sb = new StringBuilder();
StringWriter w = new StringWriter(sb, System.Globalization.CultureInfo.InvariantCulture);
sr.Serialize(w,obj); // успех
string xml = sb.ToString();
Console.WriteLine(xml);
Видите, сколько кода пришлось написать, только для того чтобы сделать возможным сериализацию вот этой строки: obj.Child = new GrandChildClass(); Причем надо учитывать, что это всего лишь пример. В этом коде мы не сможем сериализовать obj если присвоим obj.Child значение любого другого типа кроме GrandChildClass. Для этого нам надо будет добавить в XmlAttributeOverrides переопределения атрибутов “Child” для всех остальных типов, которые предполагается использовать.
Вопросы производительности
Cоздание экземпляра класса XmlSerializer довольно дорогая операция. Дело в том, что для переданного в конструктор типа, динамически создаются и компилируются сборки, содержащие код, предназначенный для сериализации именно этого типа. В результате собственно Xml сериализация выполняется довольно быстро, а вот создание экземпляров XmlSerializer занимает очень много времени. Кроме того, созданные сборки не выгружаются в результате возникает утечка памяти. В Framework 1.0 все было совсем плохо, и поэтому в одном из приложений, которое активно использовало Xml сериализацию, я создал небольшой класс, который кэширует экземпляры XmlSerializer, используя в качестве ключа полное имя типа класса подлежащего сериализации:/// <summary>Этот маленький класс сотворил чудо. Везде где мне нужен экземпляр XmlSerializer, вместо конструктора я использую SerializerCache.GetSerializer(). В результате производительность операций xml сериализации выросла на порядок.
/// Кэш для используемых сериалайзеров
/// </summary>
internal class SerializerCache
{
private static Hashtable hash = new Hashtable();
public static XmlSerializer GetSerializer(Type type)
{
XmlSerializer res = null;
lock(hash)
{
res = hash[type.FullName] as XmlSerializer;
if(res == null)
{
res = new XmlSerializer(type);
hash[type.FullName] = res;
}
}
return res;
}
}
Во Framework 2.0 разработчики отчасти поправили положение. Теперь XmlSerializer сам кэширует создаваемые им сборки, правда происходит это только при использовании конструкторов System.Xml.Serialization.XmlSerializer(Type) System.Xml.Serialization.XmlSerializer(Type,String).
Я решил проверить, имеет ли теперь смысл использовать предложенный мной класс SerializerCache? Для этого выполним тест. Сначала сериализуем 1000 объектов DataClass создавая каждый раз новый экземпляр XmlSerialize. А затем сериализуем 1000 объектов DataClass используя SerializerCache (т.е. используя один экземпляр XmlSerializer). Измерим время, затраченное на два теста:
Direct serialization time 00:00:02.2574448
Cache serialization time 00:00:00.3135340