В
статье рассматривается методика полиморфной сериализации/десериализации
объектов в XML. Приложение к статье - исходные тексты класса,
реализующего данную методику.
Описание задачи
Предположим, у нас есть иерархия классов предметной области для
представления информации о физических и юридических лицах.
Соответствующая диаграмма классов приведена на Рис. 1.
Рис. 1 Диаграмма классов физических/юридических лиц
Есть абстрактный класс BaseBody и два наследника - PrivateBody
(физическое лицо), CorporateBody (юридическое лицо). Все лица обладают
целочисленными уникальными идентификаторами (атрибут Id на диаграмме),
могут иметь несколько телефонов и несколько адресов. Физические лица
дополнительно характеризуются фамилией, именем, отчеством, датой
рождения. Юридические лица - названием, ИНН, БИК.
Объекты классов PrivateBody и CorporateBody сериализуются в XML,
сериализованное представление может размещаться в базе данных, файлах на
диске - хранилище данных значения не имеет. Приложение, предназначенное
для учета клиентов, должно восстанавливать объекты соответствующих
классов из XML.
Задача XML-сериализации/десериализации объектов в .Net решается с
помощью класса XmlSerializer. В простейшем случае, когда нам априори
известен класс восстанавливаемого из потока объекта:
XmlSerializer deserializer = new XmlSerializer(typeof(PrivateBody));
StringReader reader = new StringReader(xmlContent);
PrivateBody body = (PrivateBody) deserializer.Deserialize(reader);
Желательно, чтобы процесс восстановления объекта был полиморфным, то
есть мы получаем поток, о котором известно, что он содержит
сериализованное XML-представление одного из наследников базового класса
BaseBody, и нам необходимо восстановить объект соответствующего класса.
Стандартными средствами XmlSerializer добиться полиморфной
десериализации невозможно, так как в конструкторе XmlSerializer
необходимо указывать конкретный класс объектов, которые будут
восстанавливаться из потока, при этом если в потоке окажется наследник
заданного класса, то XmlSerializer не сможет выполнить восстановление, и
вызов завершится исключительной ситуацией. Таким образом, следующий код
не пригоден для использования:
XmlSerializer deserializer = new XmlSerializer(typeof(BaseBody));
StringReader reader = new StringReader(xmlContent);
BaseBody body = (BaseBody) deserializer.Deserialize(reader);
Методика решения
Для решения задачи полиморфной десериализации объектов из XML
разработаем свой вспомогательный класс, назовем его
PolymorphousXmlSerializer. Нам понадобятся два статических метода.
Метод Serialize является простой оберткой вызова XmlSerializer.
/// <summary>
/// Сериализация объекта в поток
/// </summary>
/// <param name="writer">Поток для сериализации</param>
/// <param name="obj">Объект</param>
public static void Serialize(TextWriter writer, object obj)
{
XmlSerializer serializer = new XmlSerializer(obj.GetType());
serializer.Serialize(writer, obj);
}
/// <summary>
/// Десериализация объекта из потока
/// </summary>
/// <param name="reader">Поток для десериализации</param>
/// <param name="baseType">Базовый класс объекта</param>
/// <param name="useDerivatives">Использовать ли наследников</param>
/// <returns>Объект</returns>
public static object Deserialize(TextReader reader, Type baseType,
bool useDerivatives)
Реализация метода Deserialize немного сложнее, хотя в конечном счете
все сводится к вызову того же XmlSerializer. Параметр useDerivatives
определяет, следует ли использовать полиморфный механизм десериализации.
Если useDerivatives = false, то вызов метода Deserialize полностью
эквивалентен стандартному механизму работы XmlSerializer.
Значительно интереснее вызов метода с useDerivatives = true. В этом
случае мы предполагаем, что в качестве baseType нам передали базовый
класс, а поток может содержать сериализованное представление любого
наследника baseType. Информацию о том, какой из наследников baseType
сериализован в потоке, можно получить из названия корневого тега XML -
оно соответствует имени класса либо значению XmlTypeAttribute.TypeName,
если для класса задан указанный атрибут сериализации. Зная базовый тип и
название корневого тега XML, мы можем определить требуемый класс
наследника, затем создать XmlSerializer с передачей в качестве параметра
конструктору класса наследника и вызвать стандартный метод
десериализации. С учетом сказанного код метода Deserialize будет иметь
следующий вид:
public static object Deserialize(TextReader reader, Type baseType,
bool useDerivatives)
{
Type serializerType = null;
string xmlContent = reader.ReadToEnd();
if (useDerivatives)
{
Match rootMatch = RootElementRegex.Match(xmlContent);
string rootName = rootMatch.Groups["1"].Value;
serializerType = GetSerializerType(baseType, rootName);
}
if (serializerType == null)
{
serializerType = baseType;
}
XmlSerializer deserializer = new XmlSerializer(serializerType);
return deserializer.Deserialize(new StringReader(xmlContent));
}
Для определения названия корневого тега в XML используется регулярное
выражение:
// Регулярное выражение для поиска названия root-элемента в XML файле
protected static readonly Regex RootElementRegex =
new Regex("<\\s*(\\w+)", RegexOptions.Compiled);
Класс наследника определяется с помощью вспомогательного метода
GetSerializerType.
/// <summary>
/// Возвращает тип, подходящий для сериализации объектов baseType
/// с root-элементом rootName
/// </summary>
/// <param name="baseType">Базовый тип</param>
/// <param name="rootName">root-элемент сериализованного в XML
/// объекта
/// </param>
/// <returns></returns>
public static Type GetSerializerType(Type baseType, string rootName)
В методе GetSerializerType нам необходимо найти всех наследников от
baseType, а затем выбрать того наследника, сериализованное
XML-представление которого будет иметь корневой элемент rootName. С
помощью Reflection мы можем найти всех наследников baseType в текущем
домене приложения (AppDomain). Для увеличения скорости работы метода
поиск наследников базового класса будем проводить только при первом
вызове, а затем кэшировать информацию о типах в обычной хэш-таблице.
Для упрощения структуры кэша и механизма поиска мы будем искать
наследников не от baseType, а от класса, являющегося вершиной иерархии
(после Object), в которой находится и baseType. Выбор наследника,
соответствующего rootName, будет осуществляться по имени класса либо по
значению атрибута XmlTypeAttribute.TypeName. Код метода
GetSerializerType выглядит следующим образом:
public static Type GetSerializerType(Type baseType, string rootName)
{
//Определяем базовый тип - вершину иерархии
Type theMostBaseType = baseType;
while (!theMostBaseType.BaseType.Equals(typeof(object)))
{
theMostBaseType = theMostBaseType.BaseType;
}
Guid baseTypeGuid = theMostBaseType.GUID;
if (BaseTypeDerivatives.Contains(baseTypeGuid))
{
return (Type) ((Hashtable)
BaseTypeDerivatives[baseTypeGuid])[rootName];
}
else
{
AppDomain currentDomain = AppDomain.CurrentDomain;
Assembly[] assemblies = currentDomain.GetAssemblies();
Hashtable typeTable = new Hashtable();
TypeFilter derivativesFilter =
new TypeFilter(FilterDerivatives);
foreach (Assembly assembly in assemblies)
{
Module[] modules = assembly.GetModules(false);
foreach (Module m in modules)
{
Type[] types = m.FindTypes(derivativesFilter,
theMostBaseType);
foreach (Type type in types)
{
XmlTypeAttribute typeAttribute =
(XmlTypeAttribute)
Attribute.GetCustomAttribute(type,
typeof(XmlTypeAttribute));
if ((typeAttribute != null) &&
(typeAttribute.TypeName != null) &&
(typeAttribute.TypeName != ""))
typeTable.Add(typeAttribute.TypeName,
type);
else
typeTable.Add(type.Name, type);
}
}
}
BaseTypeDerivatives[baseTypeGuid] = typeTable;
return (Type) typeTable[rootName];
}
}
В качестве кэша информации о типах используется следующая
хэш-таблица:
// Кэш для информации о типах и их наследниках
protected static Hashtable BaseTypeDerivatives =
Hashtable.Synchronized(new Hashtable());
Дополнительно к классу PolymorphousXmlSerializer добавлен метод для
сброса кэша:
/// <summary>
/// Сброс кэша с информацией о типах и их наследниках
/// </summary>
public static void ResetTypeCache()
{
BaseTypeDerivatives.Clear();
}
В процессе поиска наследников используется callback-функция
следующего вида:
/// <summary>
/// Делегат для фильтрации наследников заданного класса
/// </summary>
/// <param name="type">Проверяемый тип</param>
/// <param name="baseType">Критерий фильтрации</param>
/// <returns></returns>
protected static bool FilterDerivatives(Type type, object baseType)
{
return type.IsSubclassOf((Type) baseType);
}
Вот собственно и весь код, остается только добавить перегруженный
метод Deserialize, который будет автоматически использовать полиморфный
механизм десериализации.
/// <summary>
/// Десериализация объекта из потока
/// </summary>
/// <param name="reader">Поток</param>
/// <param name="baseType">Базовый класс объекта</param>
/// <returns>Объект</returns>
public static object Deserialize(TextReader reader, Type baseType)
{
return Deserialize(reader, baseType, true);
}
Теперь полиморфная десериализация объектов физических и юридических
лиц будет иметь вид:
StringReader reader = new StringReader(xmlContent);
BaseBody body = (BaseBody)PolymorphousXmlSerializer.Deserialize(reader,
typeof(BaseBody));
Заключение
Рассмотренная методика полиморфной десериализации объектов из XML
может иметь множество вариантов практического применения. В следующей
статье будет рассмотрен один нестандартный вариант использования
полиморфной десериализации в задаче конвертирования прокси-классов для
веб-сервисов.