Pull to refresh

Маппинг в C# на примере сериализатора для AMF

Reading time 26 min
Views 20K
Приветствую, друзья. Сегодня речь пойдёт о реализации маппинга на C#, а так же о применении сей реализации в решении реальных задач на примере отправки данных AMF на сервер. Всё нижеизложенное не претендует на какие-либо эталоны реализации алгоритмов и паттернов проектирования кода, это лишь описание одного из множества, далеко не всегда очевидных для новичков, решений.

В процессе изучения статьи, Вы узнаете как реализовать собственные атрибуты и как их применять, познакомитесь с методами расширений типов и применением рефлексии на практике, узнаете об основах MSIL в целом и OpCodes в частности, а так же о том, как можно сериализовать объекты в AMF с помощью потоков.

Постановка задачи


Для начала, давайте составим примерный список задач, которые нам необходимо будет решать в данной статье. Пускай он будет выглядеть следующим образом:

  • Возможность описать любые типы данных сервера в коде C# и удобно работать с ними;
  • Возможность сериализации/десериализации любых типов (как описанных нами, так и нет);
  • Сам механизм отправки данных на сервер.

В рамках статьи мы с Вами будем работать с неким готовым абстрактным сервером (в вашем случае — это вполне реальный действующий сервер, реализованый без вашего участия). Допустим, нам известно, что сервер-сайд написан на Flex, так же мы уже получили прямую ссылку, по которой нам нужно отправить AMF-объект. Для работы непосредственно с самим AMF у нас есть замечательный фреймворк FluorineFX, обеспечивающий нас всем необходимым инструментарием для огранизации поддержки Flex как на стороне сервера, так и на стороне клиента. Но вот беда, когда дело подходит к тому самому долгожданному моменту отправки объекта на сервер, выясняется, что в FluorineFX не предусмотрено удобных средств для маппинга, а все наши отправляемые на сервер объекты полетят с изначальными метаданными типов. По сути это совершенно не является проблемой, если автором клиент- и сервер-сайда являетесь Вы, но как быть в ситуациях, когда необходимо отправлять пакеты данных, имеющие кастомную структуру? Повторять 1 в 1 нэймспэйсы и имена типов не всегда удобно (а порой и вовсе невозможно). Немного погуглив, находим вполне рабочее решение неподалёку. Сразу рассмотрим его основные плюсы и минусы:

Плюсы:

  • Скорость. Напрямую вносим правки в исходный код FluorineFX, собираем и радуемся.

Минусы:

  • Поддержка. Если Вы работаете в команде, Вам придётся позаботиться о том, чтобы исходники FluorineFX всегда были доступны всем участникам командной разработки и всегда сохраняли актуальность. Это влечёт за собой больший объём проектного кода, а следовательно и больше времени на его поддержку;

  • Обновление. Если выйдет новая версия FluorineFX, Вам придётся заново обновлять весь исходный код и вносить в него правки, сохраняя при этом максимальную совместимость, что крайне не удобно. Здесь, пожалуй, единственные позитивные моменты — далеко не всегда может потребоваться новый функционал из обновлений, а сам фреймворк крайне редко обновляется. Но для оптимального рабочего процесса (а так же для случаев, когда похожая ситуация может произойти при решении иной задачи) этот момент вполне критичен.

Для тех, кого минусы данного решения не смущают, весь изложенный ниже материал, возможно, не понадобится. Для тех же, кто решил вместе со мной пойти обходным путём и найти более гибкое решение, материал, приведенный ниже, будет весьма любопытен.

Работа с атрибутами


В целом, подход, описанный в решении идёт в верном направлении. Маппинг сам по себе подразумевает некую привязку на уровне метаданных типов, для этого нам понадобятся атрибуты.

Давайте сперва составим примерное описание того, как наши типы должны выглядеть при подготовке оных к сериализации в AMF. Предусмотрим следующие возможности:

  • Типы, для которых задан атрибут, сериализуются с именем, заданным в свойстве атрибута;
  • Типы, для которых не задан атрибут (либо те атрибуты, у которых свойство равно null), сериализуются с изначальным именем типа, определённого в оригинальной сборке;
  • Свойства и поля типов, для которых задан атрибут, сериализуются с именем, заданным в свойстве атрибута;
  • Свойства и поля типов, для которых задан атрибут, но свойство атрибута при этом равно null, сериализуются с изначальным именем типа, определённого в оригинальной сборке;
  • Свойства и поля типов, для которых не задан атрибут, в процессе сериализации не участвуют и в состав членов конечного типа не входят;
  • Словари должны сериализоваться как ассоциативные массивы AMF.
  • Любые типы массивов должны сериализоваться как массивы объектов AMF.
  • Built-in-типы должны сериализоваться как built-in-типы объектов AMF.

Начнём с реализации класса наших атрибутов. Мы можем написать единый атрибут для типов объектов и типов полей и свойств объектов (аналогично решению), но я рекомендую реализовать отдельные атрибуты для типов и членов типов, что в будущем пригодится для случаев, когда необходимо разделять логику обработки метаданных у типов различных экземпляров. Сперва реализуем класс атрибута для типов объектов:

/// <summary>
/// Представляет атрибут сериализации экземпляра объекта AMF.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class AmfObjectAttribute : Attribute
{
	/// <summary>
	/// Имя типа объекта.
	/// </summary>
	public string Name { get; set; }

	/// <summary>
	/// Инициализирует новый экземпляр класса <see cref="AmfObjectAttribute"/>.
	/// </summary>
	/// <param name="name">Имя типа объекта.</param>
	public AmfObjectAttribute(string name)
	{
		Name = name;
	}

	/// <summary>
	/// Инициализирует новый экземпляр класса <see cref="AmfObjectAttribute"/>.
	/// </summary>
	public AmfObjectAttribute() : this(null) { }
}

Пара ключевых моментов в реализации атрибутов:

  • Любой атрибут должен наследоваться от класса System.Attribute;
  • Атрибут AttributeUsageAttribute необходим для определения области применения атрибута. С его помощью можно задавать и исключать типы метаданных, к которым реализуемый атрибут будет применим;
  • Если не задавать конструктор по умолчанию, нельзя будет явно присваивать значения свойствам атрибута при его инициализации, только через перегруженный конструктор с параметрами;
  • При указании типу атрибута, суффикс ...Attribute можно опустить.

Атрибут AmfObjectAttribute мы будем применять к типам объектов. Если данный атрибут будет применён, то имя типа у сериализованного объекта будет совпадать со значением, указанным в свойстве AmfObjectAttribute.Name, если значение свойства равно null — у сериализованного объекта останется оригинальное имя типа. Я намеренно не стал делать проверку наличия атрибута у типов, дабы реализовать возможность сериализации объектов типов, не помеченных данным атрибутом.

Теперь реализуем класс атрибута для свойств и полей у типов:

/// <summary>
/// Представляет атрибут для сериализации полей и свойств экземпляра объекта AMF.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class AmfMemberAttribute : Attribute
{
	/// <summary>
	/// Имя свойства или поля.
	/// </summary>
	public string Name { get; set; }

	/// <summary>
	/// Инициализирует новый экземпляр класса <see cref="AmfMemberAttribute"/>.
	/// </summary>
	/// <param name="name">Имя свойства или поля.</param>
	public AmfMemberAttribute(string name)
	{
		Name = name;
	}

	/// <summary>
	/// Инициализирует новый экземпляр класса <see cref="AmfMemberAttribute"/>.
	/// </summary>
	public AmfMemberAttribute() : this(null) { }
}

С реализацией атрибутов разобрались, теперь давайте сразу напишем простенький класс, описывающий некую модель данных. В дальнейшем мы будем использовать его для тестов:

/// <summary>
/// Представляет класс AMF-объекта для примера. После сериализации имя типа у объекта должно стать "namespase.of.your.object".
/// </summary>
[AmfObject("namespase.of.your.object")]
public class CustomAmfObject
{
	/// <summary>
	/// Тестовое свойство типа <see cref="bool"/>. После сериализации имя свойства должно стать "bit_prop".
	/// </summary>
	[AmfMember("bit_prop")]
	public bool BooleanProperty { get; set; } = true;

	/// <summary>
	/// Тестовое свойство типа <see cref="sbyte"/>. После сериализации имя свойства должно остаться UnsignedByteProperty.
	/// </summary>
	[AmfMember]
	public sbyte UnsignedByteProperty { get; set; } = 2;
	
	/// <summary>
	/// Тестовое свойство типа <see cref="string"/>. В сериализации не участвует.
	/// </summary>
	public string StringProperty { get; set; } = "test";

	/// <summary>
	/// Тестовое поле типа <see cref="bool"/>. После сериализации имя поля должно стать "bit_fld".
	/// </summary>
	[AmfMember("bit_fld")]
	public bool booleanField = false;

	/// <summary>
	/// Тестовое поле типа <see cref="float"/>. После сериализации имя свойства должно остаться singleField.
	/// </summary>
	[AmfMember]
	public float singleField = -5.00065f;

	/// <summary>
	/// Тестовое поле типа <see cref="string"/>. В сериализации не участвует.
	/// </summary>
	public string stringField = "test2";

	/// <summary>
	/// Инициализирует новый экземпляр класса <see cref="CustomAmfObject"/>.
	/// </summary>
	public CustomAmfObject() { }
}

Атрибуты написаны, тестовый класс реализован. Теперь нам нужно приступить к решению задачи непосредственно самой сериализации. Если планируется поддержка версии .NET 2.0, то нам необходимо реализовать класс-сериализатор, который будет работать с экземплярами объектов и проводить с метаданными их типов различные манипуляции. Мы же будем писать код с учётом поддержки версии .NET 3.5, ибо в нём появились две очень значимые для C#-инженера возможности: LINQ и методы расширений типов. Их для решения нашей задачи мы и применим.

Методы расширений и рефлексия


Чтобы реализовать метод расширения типа, достаточно объявить публичный статический класс и добавить к первому параметру модификатор this. Эти условия являются обязательными, иначе компилятор просто не поймёт, что метод является расширением типа. После реализации, метод можно будет применить к любому объекту, тип которого совпадает или наледуется от типа первого параметра метода расширения. Создадим наш собственный класс методов расширений:

/// <summary>
/// Представляет набор расширений для работы с сериализацией/десериализацией объектов AMF.
/// </summary>
public static class Extensions { }

Первым делом, нам понадобится несколько вспомогательных методов для повторного использования кода. В .NET 4.5 появился метод Attribute Type.GetCustomAttribute(Type), который позволяет сразу получить атрибут заданного типа. В .NET 3.5 такого ещё нет, так что реализуем пару методов расширений для удобной работы с атрибутами:

/// <summary>
/// Находит и возвращает атрибут.
/// </summary>
/// <typeparam name="T">Тип искомого атрибута.</typeparam>
/// <param name="sourceType">Исходный тип для поиска.</param>
/// <returns></returns>
private static T GetAttribute<T>(this Type sourceType) where T : Attribute
{
	object[] attributes = sourceType.GetCustomAttributes(typeof(T), true);  // Получаем текущий атрибут.

	if (attributes == null || attributes.Length == 0) return default(T); // Если у типа объекта не задан атрибут - возвращаем null.

	return attributes[0] as T;
}

/// <summary>
/// Находит и возвращает атрибут.
/// </summary>
/// <typeparam name="T">Тип искомого атрибута.</typeparam>
/// <param name="sourceMember">Исходный тип для поиска.</param>
/// <returns></returns>
private static T GetAttribute<T>(this MemberInfo sourceMember) where T : Attribute
{
	object[] attributes = sourceMember.GetCustomAttributes(typeof(T), true);  // Получаем текущий атрибут.

	if (attributes == null || attributes.Length == 0) return default(T); // Если у типа объекта не задан атрибут - возвращаем null.

	return attributes[0] as T;
}

/// <summary>
/// Определяет, задан ли указанный атрибут типу.
/// </summary>
/// <typeparam name="T">Тип атрибута.</typeparam>
/// <param name="sourceType">Исходный тип объекта.</param>
/// <returns></returns>
private static bool IsDefinedAttribute<T>(this Type sourceType)
{
	object[] attributes = sourceType.GetCustomAttributes(typeof(T), true);  // Получаем текущий атрибут.
	return attributes != null && attributes.Length > 0;
}

При каждой процедуре сериализации будет происходить генерация типов по заданным атрибутам. Дабы не повторять множество операций при каждом вызове метода сериализации, давайте сразу предусмотрим возможность хранения метаданных в динамической сборке после её создания:

/// <summary>
/// Билдер для модуля динамической сборки.
/// </summary>
private static ModuleBuilder moduleBuilder;

/// <summary>
/// Статический конструктор класса <see cref="Extensions"/>.
/// </summary>
static Extensions()
{
	AssemblyName assemblyName = new AssemblyName("AmfDynamicAssembly");    // Создаём новую среду выполнения кода.
	AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);    // Определяем среду выполнения.
	moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll");   // Определяем новый модуль для среды выполнения.
}

Теперь для всех типов у нас будет собственная единая динамическая сборка, которая будет хранить их метаданные. Самое время реализовать алгоритм генерации типов на основе данных наших атрибутов. Сперва пошагово разберём каждый этап алгоритма, затем я приведу полный листинг метода.

В основе самого понятия «рефлексии» (оно же «рефлекшн», оно же «отражение») и лежат манипуляции с метаданными типов. Здесь, по сути, всё просто. Нам нужно повторить процесс создания описанных в коде данных типов компилятором, но уже с помощью собственного кода. За основу будем брать метаданные известного нам типа исходного объекта, а так же данные из атрибутов, если они имеются. Так же нам понадобится реализовать конструктор по умолчанию, инициализирующий ссылку на экземпляр объекта генерируемого типа. Все необходимые нам операции мы можем осуществить с помощью класса ModuleBuilder.TypeBuilder.

Определяем тип с помощью TypeBuilder:

TypeBuilder typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);

Определяем поле с помощью FieldBuilder:

FieldBuilder fieldBuilder = typeBuilder.DefineField($"m_{propertyName}", propertyType, FieldAttributes.Private);

Определяем свойство с помощью PropertyBuilder. Здесь нам нужно определить приватное поле, а так же аксессор и мутатор для доступа к нему:

PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); // Определяем новое свойство.
MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;  // Устанавливаем атрибуты аксессору и мутатору свойства.

MethodBuilder methodBuilderAccessor = typeBuilder.DefineMethod($"get_{propertyName}", getSetAttr, propertyType, Type.EmptyTypes);  // Определяем аксессор.
ILGenerator accessorIL = methodBuilderAccessor.GetILGenerator();   // Получаем ссылку на генератор MSIL-инструкций для аксессора.
accessorIL.Emit(OpCodes.Ldarg_0);     // Помещаем в стек вычислений нулевой аргумент. 
accessorIL.Emit(OpCodes.Ldfld, fieldBuilder);   // Помещаем в стек вычислений инструкцию о получении значения по ссылке поля.
accessorIL.Emit(OpCodes.Ret);     // Помещаем в стек вычислений инструкцию о возврате из метода.
MethodBuilder methodBuilderSetter = typeBuilder.DefineMethod($"set_{propertyName}", getSetAttr, null, new Type[] { propertyType });    // Определяем мутатор.
ILGenerator setterIL = methodBuilderSetter.GetILGenerator();    // Получаем ссылку на генератор MSIL-инструкций для мутатора.
setterIL.Emit(OpCodes.Ldarg_0);   // Помещаем в стек вычислений нулевой аргумент.
setterIL.Emit(OpCodes.Ldarg_1); // Помещаем в стек вычислений первый аргумент.
setterIL.Emit(OpCodes.Stfld, fieldBuilder); // Помещаем в стек вычислений инструкцию о сохранении значения по ссылке поля.
setterIL.Emit(OpCodes.Ret);   // Помещаем в стек вычислений инструкцию о возврате из метода.

propertyBuilder.SetGetMethod(methodBuilderAccessor);    // Добавляем свойству аксессор.
propertyBuilder.SetSetMethod(methodBuilderSetter);  // Добавляем свойству мутатор.

Определяем конструктор по умолчанию с помощью ConstructorBuilder:

ConstructorBuilder ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);  // Определяем конструктор.
ILGenerator ctorIL = ctor.GetILGenerator();   // Получаем ссылку на генератор MSIL-инструкций для конструктора.
ctorIL.Emit(OpCodes.Ldarg_0);  // Помещаем в стек вычислений нулевой аргумент. 
ctorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); // Вызываем базовый конструктор для инициализации значения по умолчанию у нулевого аргумента.
ctorIL.Emit(OpCodes.Ret);  // Помещаем в стек вычислений инструкцию о возврате из метода.

Инициализируем новый экземпляр объекта только что сгенерированного типа с помощью Activator:

object targetObject = Activator.CreateInstance(typeBuilder.CreateType());

У всех сборщиков метаданных методов есть метод (почти каламбур) GetILGenerator(), возвращающий экземпляр ILGenerator, который позволяет нам поместить в стек вычислений MSIL нужную последовательность операторов на выполнение. От набора инструкций IL, которые мы передаём из OpCodes, зависит напрямую то, как поведёт себя логика описываемого метода. В данной статье я лишь поверхностно затрагиваю тему рефлексии, это повод для уже отдельной статьи, кроме того Вы всегда сможете ознакомиться с полным перечнем инструкций в официальной документации MSDN.

Теперь у нас есть всё необходимое для написания логики динамической генерации метаданных типа. Сразу учтём тот факт, что свойства и поля у генерируемого типа так же могут иметь наш атрибут сериализации, для реализации применим рекурсию. Если у объекта не задан атрибут сопоставления имён типов — возвращаем его как есть. Если в динамической сборке уже имеются метаданные нашего типа — собираем экземпляр объекта на их основе. Это всё полезно для оптимизации. Полный листинг метода генерации, со всеми необходимыми проверками и процедурами, выглядит следующим образом:

/// <summary>
/// Генерирует объект с метаданными типа в соответствии с заданными атрибутами <see cref="AmfObjectAttribute"/>, с полями типа, заданного в атрибутах <see cref="AmfMemberAttribute"/>.
/// </summary>
/// <param name="sourceObject">Исходный экземпляр объекта.</param>
/// <returns></returns>
private static object GenerateType<T>(T sourceObject)
{
    Type sourceType = sourceObject.GetType(); // Получаем метаданные типа исходного объекта.

    if (sourceType.IsDictionary()) return GenerateType(sourceObject as IEnumerable<KeyValuePair<string, object>>);
    if (!sourceType.IsDefinedAttribute<AmfObjectAttribute>()) return sourceObject; // Если у типа объекта не задан атрибут - возвращаем как есть.

    string typeName = sourceType.GetAttribute<AmfObjectAttribute>().Name ?? sourceType.FullName;    // Определяем имя у типа.
    Type definedType = moduleBuilder.GetType(typeName);   // Пытаемся найти уже определенный в сборке тип.
    TypeBuilder typeBuilder = null; // Определяем билдер для нашего типа.

    Dictionary<string, object> properties = new Dictionary<string, object>();   // Словарь свойств объекта.
    Dictionary<string, object> fields = new Dictionary<string, object>();   // Словарь полей объекта.

    // Если тип в сборке еще не определен...
    if (definedType == null)
    {
        typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);  // Опледеляем тип с нашим именем.

        ConstructorBuilder ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);  // Определяем конструктор.
        ILGenerator ctorIL = ctor.GetILGenerator();   // Получаем ссылку на генератор MSIL-инструкций для конструктора.
        ctorIL.Emit(OpCodes.Ldarg_0);  // Помещаем в стек вычислений нулевой аргумент. 
        ctorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); // Вызываем базовый конструктор для инициализации значения по умолчанию у нулевого аргумента.
        ctorIL.Emit(OpCodes.Ret);  // Помещаем в стек вычислений инструкцию о возврате из метода.

        // Перебираем все свойства нашего типа.
        foreach (PropertyInfo propertyInfo in sourceType.GetProperties())
        {
            AmfMemberAttribute attribute = propertyInfo.GetAttribute<AmfMemberAttribute>();  // Получаем наш кастомный атрибут типа AmfMemberAttribute.

            if (attribute == null) continue; // Если атрибут не указан - пропускаем свойство.

            string propertyName = attribute.Name ?? propertyInfo.Name; // Получаем имя свойства.
            object propertyValue = propertyInfo.GetValue(sourceObject, null); // Получаем значение свойства.
            Type propertyType = propertyInfo.PropertyType;  // Получаем метаданные типа свойства.

            // Если у типа задан атрибут или это словарь...
            if (propertyInfo.PropertyType.IsDefinedAttribute<AmfObjectAttribute>() || propertyType.IsDictionary())
            {
                // Генерируем объект типа, заданного в атрибуте.
                propertyValue = propertyType.IsDictionary()
                    ? GenerateType(propertyValue as IEnumerable<KeyValuePair<string, object>>)
                    : GenerateType(propertyValue);

                propertyType = propertyValue.GetType();   // Обновляем тип свойства.
            }

            FieldBuilder fieldBuilder = typeBuilder.DefineField($"m_{propertyName}", propertyType, FieldAttributes.Private);   // Определяем новое приватное поле.
            PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); // Определяем новое свойство.
            MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;  // Устанавливаем атрибуты аксессору и мутатору свойства.

            MethodBuilder methodBuilderAccessor = typeBuilder.DefineMethod($"get_{propertyName}", getSetAttr, propertyType, Type.EmptyTypes);  // Определяем аксессор.
            ILGenerator accessorIL = methodBuilderAccessor.GetILGenerator();   // Получаем ссылку на генератор MSIL-инструкций для аксессора.
            accessorIL.Emit(OpCodes.Ldarg_0);     // Помещаем в стек вычислений нулевой аргумент. 
            accessorIL.Emit(OpCodes.Ldfld, fieldBuilder);   // Помещаем в стек вычислений инструкцию о получении значения по ссылке поля.
            accessorIL.Emit(OpCodes.Ret);     // Помещаем в стек вычислений инструкцию о возврате из метода.
            MethodBuilder methodBuilderSetter = typeBuilder.DefineMethod($"set_{propertyName}", getSetAttr, null, new Type[] { propertyType });    // Определяем мутатор.
            ILGenerator setterIL = methodBuilderSetter.GetILGenerator();    // Получаем ссылку на генератор MSIL-инструкций для мутатора.
            setterIL.Emit(OpCodes.Ldarg_0);   // Помещаем в стек вычислений нулевой аргумент.
            setterIL.Emit(OpCodes.Ldarg_1); // Помещаем в стек вычислений первый аргумент.
            setterIL.Emit(OpCodes.Stfld, fieldBuilder); // Помещаем в стек вычислений инструкцию о сохранении значения по ссылке поля.
            setterIL.Emit(OpCodes.Ret);   // Помещаем в стек вычислений инструкцию о возврате из метода.

            propertyBuilder.SetGetMethod(methodBuilderAccessor);    // Добавляем свойству аксессор.
            propertyBuilder.SetSetMethod(methodBuilderSetter);  // Добавляем свойству мутатор.

            properties.Add(propertyName, propertyValue);  // Сохраняем значения в словарь для дальнейшей передачи свойствам значений.
        }

        // Перебираем все поля нашего типа.
        foreach (FieldInfo fieldInfo in sourceType.GetFields())
        {
            AmfMemberAttribute attribute = fieldInfo.GetAttribute<AmfMemberAttribute>();  // Получаем наш кастомный атрибут типа AmfMemberAttribute.

            if (attribute == null) continue; // Если атрибут не указан - пропускаем поле.

            string fieldName = attribute.Name ?? fieldInfo.Name; // Получаем имя поля.
            object fieldValue = fieldInfo.GetValue(sourceObject); // Получаем значение поля.
            Type fieldType = fieldInfo.FieldType;  // Получаем метаданные типа поля.

            // Если у типа задан атрибут или это словарь...
            if (fieldInfo.FieldType.IsDefinedAttribute<AmfObjectAttribute>() || fieldType.IsDictionary())
            {
                // Генерируем объект типа, заданного в атрибуте.
                fieldValue = fieldType.IsDictionary()
                    ? GenerateType(fieldValue as IEnumerable<KeyValuePair<string, object>>)
                    : GenerateType(fieldValue);

                fieldType = fieldValue.GetType();   // Обновляем тип поля.
            }

            typeBuilder.DefineField(fieldName, fieldType, FieldAttributes.Public);   // Определяем новое поле.
            fields.Add(fieldName, fieldValue);  // Сохраняем значения в словарь для дальнейшей передачи свойствам значений.
        }
    }
    else
    {
        // Перебираем все свойства нашего типа.
        foreach (PropertyInfo propertyInfo in sourceType.GetProperties())
        {
            AmfMemberAttribute attribute = propertyInfo.GetAttribute<AmfMemberAttribute>();  // Получаем наш кастомный атрибут типа AmfMemberAttribute.

            if (attribute == null) continue; // Если атрибут не указан - пропускаем свойство.

            string propertyName = attribute.Name ?? propertyInfo.Name; // Получаем имя свойства.
            object propertyValue = propertyInfo.GetValue(sourceObject, null); // Получаем значение свойства.
            Type propertyType = propertyInfo.PropertyType;  // Получаем метаданные типа свойства.

            AmfObjectAttribute propertyAttribute = propertyInfo.PropertyType.GetAttribute<AmfObjectAttribute>();  // Получаем атрибут у свойства.

            // Если у типа задан атрибут или это словарь...
            if (propertyAttribute != null || propertyType.IsDictionary())
            {
                // Генерируем объект типа, заданного в атрибуте.
                propertyValue = propertyType.IsDictionary()
                    ? GenerateType(propertyValue as IEnumerable<KeyValuePair<string, object>>)
                    : GenerateType(propertyValue);

                propertyType = propertyValue.GetType();   // Обновляем тип свойства.
            }

            properties.Add(propertyName, propertyValue);  // Сохраняем значения в словарь для дальнейшей передачи свойствам значений.
        }

        // Перебираем все поля нашего типа.
        foreach (FieldInfo fieldInfo in sourceType.GetFields())
        {
            AmfMemberAttribute attribute = fieldInfo.GetAttribute<AmfMemberAttribute>();  // Получаем наш кастомный атрибут типа AmfMemberAttribute.

            if (attribute == null) continue; // Если атрибут не указан - пропускаем поле.

            string fieldName = attribute.Name ?? fieldInfo.Name; // Получаем имя поля.
            object fieldValue = fieldInfo.GetValue(sourceObject); // Получаем значение поля.
            Type fieldType = fieldInfo.FieldType;  // Получаем метаданные типа поля.

            AmfObjectAttribute fieldAttribute = fieldInfo.FieldType.GetAttribute<AmfObjectAttribute>();  // Получаем атрибут у поля.

            // Если у типа задан атрибут или это словарь...
            if (fieldAttribute != null || fieldType.IsDictionary())
            {
                // Генерируем объект типа, заданного в атрибуте.
                fieldValue = fieldType.IsDictionary()
                    ? GenerateType(fieldValue as IEnumerable<KeyValuePair<string, object>>)
                    : GenerateType(fieldValue);

                fieldType = fieldValue.GetType();   // Обновляем тип поля.
            }

            fields.Add(fieldName, fieldValue);  // Сохраняем значения в словарь для дальнейшей передачи свойствам значений.
        }
    }

    object targetObject = Activator.CreateInstance(definedType ?? typeBuilder.CreateType());  // Создаём инстанс нашего динамического типа.

    // Раставляем значения всем свойствам объекта.
    foreach (KeyValuePair<string, object> property in properties) targetObject.GetType().GetProperty(property.Key).SetValue(targetObject, property.Value, null);

    // Раставляем значения всем полям объекта.
    foreach (KeyValuePair<string, object> field in fields) targetObject.GetType().GetField(field.Key).SetValue(targetObject, field.Value);

    return targetObject;
}

Теперь нам нужно позаботиться о том, чтобы метаданные массивов объектов корректно сохранялись при сериализации. Для этого напишем элементарную перегрузку метода генерации типа:

/// <summary>
/// Генерирует массив объектов с метаданными типа в соответствии с заданными атрибутами <see cref="AmfObjectAttribute"/>, с полями типа, заданного в атрибутах <see cref="AmfMemberAttribute"/>.
/// </summary>
/// <param name="sourceObjects">Массив исходных объектов.</param>
/// <returns></returns>
private static object[] GenerateType(object[] sourceObjects)
{
	for (int i = 0; i < sourceObjects.Length; i++) sourceObjects[i] = GenerateType(sourceObjects[i]);   // Генерируем типы для каждого элемента массива.
	return sourceObjects;
}

Теперь мы уже можем сериализовать любые объекты и массивы в AMF для дальнейшей отправки на сервер. Но нам ещё необходим функционал, обеспечивающий отправку ассоциативных массивов AMF, которых в C# попросту нет. В роли ассоциативных массивов здесь выступают различные реализации словарей IEnumerable<KeyValuePair<TKey, TValue>, а так же хэштаблицы Hashtable, AMF же распознаёт каждый ключ словаря как поле у типа Array. Для реализации решения этой задачи напишем ещё одну перегрузку, которая сможет правильно сгенерировать словарь AMF на основе ключей и значений словаря:

/// <summary>
/// Определяет, является ли тип словарём или наследником от словаря. По сути это костыльная заглушка, чтобы заставить перегрузку для генерации ассоциативного массива перекрывать перегрузку для генерации остальных object, иначе на выходе всегда будем получать массив пар, не типичный для AMF.
/// </summary>
/// <param name="sourceType">Проверяемый тип.</param>
/// <returns></returns>
private static bool IsDictionary(this Type sourceType)
{
    Type type0 = typeof(IEnumerable<KeyValuePair<string, object>>);
    Type type1 = typeof(IDictionary<string, object>);
    Type type2 = typeof(Dictionary<string, object>);

    return sourceType.FullName == type0.FullName || sourceType.IsSubclassOf(type0)
        || sourceType.FullName == type1.FullName || sourceType.IsSubclassOf(type1)
        || sourceType.FullName == type2.FullName || sourceType.IsSubclassOf(type2);
}

/// <summary>
/// Генерирует объект на основе коллекции пар "ключ-значение", где, ключ - имя поля объекта.
/// </summary>
/// <param name="fields">Коллекция полей будущего объекта вида "ключ-значение"</param>
/// <returns></returns>
private static object GenerateType(IEnumerable<KeyValuePair<string, object>> fields)
{
    AssemblyName assemblyName = new AssemblyName("AmfDynamicAssemblyForDictionary");    // Создаём новую среду выполнения кода.
    AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);    // Определяем среду выполнения.
    ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll");   // Определяем новый модуль для среды выполнения.
    TypeBuilder typeBuilder = moduleBuilder.DefineType(typeof(Array).FullName, TypeAttributes.Public);  // Опледеляем тип с нашим именем.

    ConstructorBuilder ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes);  // Определяем конструктор.
    ILGenerator ctorIL = ctor.GetILGenerator();   // Получаем ссылку на генератор MSIL-инструкций для конструктора.
    ctorIL.Emit(OpCodes.Ldarg_0);  // Помещаем в стек вычислений нулевой аргумент. 
    ctorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); // Вызываем базовый конструктор для инициализации значения по умолчанию у нулевого аргумента.
    ctorIL.Emit(OpCodes.Ret);  // Помещаем в стек вычислений инструкцию о возврате из метода.

    // Перебираем все поля нашего типа.
    foreach (KeyValuePair<string, object> pair in fields)
    {
        object fieldValue = pair.Value; // Получаем значение поля.
        Type fieldType = fieldValue.GetType();  // Получаем метаданные типа поля.

        // Если у типа задан атрибут...
        if (fieldType.IsDefinedAttribute<AmfObjectAttribute>())
        {
            fieldValue = GenerateType(fieldValue); // Генерируем объект типа, заданного в атрибуте.
            fieldType = fieldValue.GetType();   // Обновляем тип поля.
        }

        typeBuilder.DefineField(pair.Key, fieldType, FieldAttributes.Public);   // Определяем новое поле.
    }

    object targetObject = Activator.CreateInstance(typeBuilder.CreateType());  // Создаём инстанс нашего динамического типа.

    // Расставляем значения всем свойствам объекта.
    foreach (KeyValuePair<string, object> pair in fields) targetObject.GetType().GetField(pair.Key).SetValue(targetObject, pair.Value);

    return targetObject;
}

Обратите внимание, здесь я создаю отдельную динамическую сборку и при каждом вызове метода переопределяю в ней метаданные. Это необходимо для того, чтобы все метаданные нашего словаря генерировались заново, иначе через какое-то время выполнения у экземпляров наших словарей будет целая куча неопределённых полей, которых в типе быть не должно и которые будут занимать место в памяти.

Сериализация в AMF


Теперь уже точно всё готово для сериализации объектов в AMF. Приступим к реализации сериализатора:

/// <summary>
/// Сериализует объект в буффер AMF.
/// </summary>
/// <param name="sourceObject">Исходный объект.</param>
/// <param name="version">Версия AMF.</param>
/// <returns></returns>
public static byte[] SerializeToAmf(this object sourceObject, ushort version)
{
	using (MemoryStream memoryStream = new MemoryStream())  // Открываем поток для записи данных в буфер.
	using (AMFSerializer amfSerializer = new AMFSerializer(memoryStream))   // Инициализируем сериализатор для AMF.
	{
		AMFMessage amfMessage = new AMFMessage(version);  // Создаём сообщение для передачи серверу с заданным номером версии AMF.
		AMFBody amfBody = new AMFBody(AMFBody.OnResult, null, GenerateType(sourceObject));    // Создаём тело для сообщения AMF.

		amfMessage.AddBody(amfBody);    // Добавляем body для сообщения AMF.
		amfSerializer.WriteMessage(amfMessage); // Сериализуем сообщение.

		return memoryStream.ToArray();  // Преобразовывает поток памяти в буфер и возвращает.
	}
}

/// <summary>
/// Сериализует объект в буффер AMF3.
/// </summary>
/// <param name="sourceObject">Исходный объект.</param>
/// <returns></returns>
public static byte[] SerializeToAmf(this object sourceObject) => sourceObject.SerializeToAmf(3);

/// <summary>
/// Сериализует объект в файл *.amf.
/// </summary>
/// <param name="sourceObject">Сериализуемый объект.</param>
/// <param name="path">Путь сохранения.</param>
/// <param name="version">Номер версии AMF.</param>
public static void SerializeToAmf(this object sourceObject, string path, ushort version)
	=> File.WriteAllBytes($"{path}.amf", sourceObject.SerializeToAmf(version));

/// <summary>
/// Сериализует объект в файл *.amf. Версия AMF равна 3.
/// </summary>
/// <param name="sourceObject">Сериализуемый объект.</param>
/// <param name="path">Путь сохранения.</param>
public static void SerializeToAmf(this object sourceObject, string path) => sourceObject.SerializeToAmf(path, 3);

Реализация почти тривиальна. Единственный нюанс — я намеренно создал перегрузку с номером версии AMF равным по умолчанию трём. Когда мы работаем с Flex, это практически всегда AMF3, а значит и нет смысла передавать в вызовах метода сериализации дополнительный аргумент.

Для десериализации необходимо проделать все вышеописанные процедуры, только в обратном порядке:

/// <summary>
/// Десериализует буфер данных в объект AMF.
/// </summary>
/// <typeparam name="T">Тип десериализуемого объекта.</typeparam>
/// <param name="sourceBuffer">Исходный буфер данных объекта.</param>
/// <returns></returns>
public static T DeserializeFromAmf<T>(this byte[] sourceBuffer) where T : class
{
	using (MemoryStream memoryStream = new MemoryStream(sourceBuffer))  // Открываем поток для чтения данных из буфера.
	using (AMFDeserializer amfDeserializer = new AMFDeserializer(memoryStream))      // Инициализируем десериализатор для AMF.
	{
		AMFMessage amfMessage = amfDeserializer.ReadAMFMessage();   // Получаем сообщение AMF.
		AMFBody amfBody = amfMessage.GetBodyAt(0);  // Получаем body из сообщения AMF.

		object amfObject = amfBody.Content; // Получаем объект из body AMF.
		Type amfObjectType = amfObject.GetType();   // Получаем метаданные типа объекта AMF.

		// Формируем запрос на получение всей коллекции нужных нам типов с заданными атрибутами.
		IEnumerable<Type> types = from type in Assembly.GetExecutingAssembly().GetTypes()
								  where Attribute.IsDefined(type, typeof(AmfObjectAttribute))
								  select type;

		Type currentType = null;   // Определяем текущий тип объекта из нашей сборки.

		// Проходим по всем найденным типам с нашим атрибутом.
		foreach (Type type in types)
		{
			AmfObjectAttribute attribute = type.GetAttribute<AmfObjectAttribute>();   // Получаем наш атрибут.

			if (attribute == null || attribute.Name != amfObjectType.FullName) continue;   // Если в атрибуте задано другое имя - пропускаем итерацию.

			currentType = type; // Иначе сохраняем текущий тип объекта.
			break;
		}

		if (currentType == null) return default(T); // Если тип не найден - возвращаем null.

		object targetObject = Activator.CreateInstance(currentType);  // Создаём инстанс нашего типа.

		// Анализируем все свойства нашего класса.
		foreach (PropertyInfo propertyInfo in currentType.GetProperties())
		{
			AmfMemberAttribute attribute = propertyInfo.GetAttribute<AmfMemberAttribute>();   // Получаем наш кастомный атрибут.

			if (attribute == null) continue;    // Если атрибут не задан - пропускаем.

			propertyInfo.SetValue(targetObject, amfObjectType.GetProperty(attribute.Name).GetValue(amfObject, null), null);   // Получаем значение свойства у десериализуемого объекта и сохраняем его в свойстве нашего объекта.
		}

		// Анализируем все поля нашего класса.
		foreach (FieldInfo fieldInfo in currentType.GetFields())
		{
			AmfMemberAttribute attribute = fieldInfo.GetAttribute<AmfMemberAttribute>();   // Получаем наш кастомный атрибут.

			if (attribute == null) continue;    // Если атрибут не задан - пропускаем.

			fieldInfo.SetValue(targetObject, amfObjectType.GetField(attribute.Name).GetValue(amfObject));   // Получаем значение поля у десериализуемого объекта и сохраняем его в поле нашего объекта.
		}

		return targetObject as T;  // Приводит к типу T и возвращает текущий объект.
	}
}

/// <summary>
/// Десериализует объект из файла *.amf.
/// </summary>
/// <typeparam name="T">Тип десериализуемого объекта.</typeparam>
/// <param name="obj">Десериализуемый объект.</param>
/// <param name="path">Путь к файлу объекта.</param>
/// <returns>Десериализованный объект AMF.</returns>
public static T DeserializeFromAmf<T>(this object obj, string path) where T : class => File.ReadAllBytes($"{path}.amf").DeserializeFromAmf<T>();

Теперь давайте посмотрим, как можно самым быстрым способом сериализовать объект в AMF и отправить его на сервер:

using (MemoryStream memoryStream = new MemoryStream())  // Резервируем буффер для записи нашего объекта и освобождаем память по завершению работы метода.
using (AMFWriter amfWriter = new AMFWriter(memoryStream))   // Открываем на запись буфер для AMF.
using (WebClient client = new WebClient())    // Открываем HTTP-сокет для отправки данных на сервер (можно и через низкоуровневый HttpWebRequest).
{
	amfWriter.WriteBytes(new CustomAmfObject().SerializeToAmf());    // Записываем данные объекта в буфер.
	client.Headers[HttpRequestHeader.ContentType] = "application/x-amf";    // Добавляем заголовок ContentType в запрос.

	byte[] buffer = client.UploadData(Host, "POST", memoryStream.ToArray());    // Отправляем данные на сервер.
}

На скриншоте ниже можно наблюдать структуру сериализованного объекта, готового к отправке на сервер:

image

В случае, если сервер после отправки данных будет ругаться на то, что объект не реализует интерфейс IExternalizable, в наши кастомные классы необходимо имплементировать реализацию IExternalizable:

    [AmfObject("example.game.gameObject")]
    public class CustomAmfObject : IExternalizable
    {
        [AmfMember("x")] public float X { get; set; }
        [AmfMember("y")] public float Y { get; set; }
        [AmfMember("z")] public float Z { get; set; }

        public CustomAmfObject(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public CustomAmfObject() : this(0f, 0f, 0f) { }

        public void ReadExternal(IDataInput input)
        {
            X = input.ReadFloat();
            Y = input.ReadFloat();
            Z = input.ReadFloat();
        }

        public void WriteExternal(IDataOutput output)
        {
            output.WriteFloat(X);
            output.WriteFloat(Y);
            output.WriteFloat(Z);
        }
    }

Как показали тесты в реальных условиях, для сериализации данных через такой подход вовсе не обязательна цепочка записей в IDataOutput и чтения из IDataInput, все данные и без них отправлялись на сервер в корректном виде. Для особо же «ругливых» серверов такое решение может оказаться весьма полезным.

Послесловие


Описанный выше способ решения задачи является далеко не единственным из тех, что не требуют вмешательства в исходный код оригинального фреймворка. Например, мы могли бы вообще обойтись без атрибутов и работы с метаданными из кода, если бы реализовывали экземпляры наших объектов с помощью DynamicObject, ExpandoObject и прочих dynamic-неприятностей, возлагая при этом всю работу с метаданными на DLR.

Базы знаний, изложенной в статье, должно хватить для написания сериализатора практически под любые задачи, либо простого интерпретатора/компилятора на основе MSIL и рефлексии, а так же более наглядного понимания принципа сериализации в AMF.

Исходники к статье доступны здесь.
NuGet-пакет для расширения FluorineFx можно найти здесь.
Если Вам нужно только сериализовать AMF и тащить ради этого весь FluorineFX в проект ой как не хочется, то здесь Вы сможете найти предложенный ArtemA вариант сериализатора, реализованный с помощью маппинга, но уже на основе DataContract-сериализации .NET.

Надеюсь, изложенный мною материал был для Вас полезен.
Благодарю за внимание!
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+14
Comments 6
Comments Comments 6

Articles