Как создать SerializationBinder для Binary Formatter, который обрабатывает перемещение типов из одной сборки и пространства имен в другое

Контекст следующий

  1. Я хочу провести рефакторинг кода, перемещая его в разные проекты.
  2. Часть этого кода состоит из сериализуемых DTO, которые используются для отправки и получения данных через несколько конечных точек.
  3. Если я перемещаю код, сериализация прерывается (поэтому он не имеет обратной совместимости со старыми версиями моего приложения).

Решением этой проблемы является SerializationBinder, который позволяет мне в некотором смысле «перенаправлять» с одного типа на другой.

Поэтому я хочу создать SerializationBinder для удовлетворения этой потребности. Однако он должен сделать это, выполнив следующие требования.

  1. Входные данные для SerializationBinder должны представлять собой список сопоставлений старого типа с новым. Сопоставление должно включать старое имя сборки (без версии, без маркера открытого ключа) и старое полное имя типа (пространство имен и имя), а также новое имя сборки и новое полное имя типа.
  2. Для типов, которые находятся во входных данных, номера версий сборок следует игнорировать.
  3. Он должен обрабатывать дженерики, если мои типы находятся в дженериках (список, словарь и т. д.), без необходимости включать дженерики во входные данные.
  4. Для всего, что не входит во входные данные (например, типы, которые не были перемещены, или типы .NET, такие как набор данных, например), по умолчанию следует использовать готовый алгоритм двоичного сериализатора.

Это возможно или мне снится? Есть ли что-то, что уже делает это? Я бы предположил, что это общая проблема.

Пока что я не вижу простого способа сделать 3 и вообще не вижу способа сделать 4.

Вот попытка

public class SmartDeserializationBinder : SerializationBinder
{
    /// <summary>
    /// Private class to handle storing type mappings
    /// </summary>
    private class TypeMapping
    {
        public string OldAssemblyName { get; set; }
        public string OldTypeName { get; set; }
        public string NewAssemblyName { get; set; }
        public string NewTypeName { get; set; }
    }

    List<TypeMapping> typeMappings;

    public SmartDeserializationBinder()
    {
        typeMappings = new List<TypeMapping>();
    }

    public void AddTypeMapping(string oldAssemblyName, string oldTypeName, string newAssemblyName, string newTypeName)
    {
        typeMappings.Add(new TypeMapping()
        {
            OldAssemblyName = oldAssemblyName,
            OldTypeName = oldTypeName,
            NewAssemblyName = newAssemblyName,
            NewTypeName = newTypeName
        });
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        //Need to handle the fact that assemblyName will come in with version while input type mapping may not
        //Need to handle the fact that generics come in as mscorlib assembly as opposed to the assembly where the type is defined.
        //Need to handle the fact that some types won't even be defined by mapping. In this case we should revert to normal Binding... how do you do that?

        string alternateAssembly = null;
        string alternateTypeName = null;
        bool needToMap = false;
        foreach (TypeMapping mapping in typeMappings)
        {
            if (typeName.Contains(mapping.OldTypeName))
            {
                alternateAssembly = mapping.NewAssemblyName;
                alternateTypeName = mapping.NewTypeName;
                needToMap = true;
                break;
            }
        }

        if (needToMap)
        {
            bool isList = false;
            if (typeName.Contains("List`1"))
                isList = true;
            // other generics need to go here

            if (isList)
                return Type.GetType(String.Format("System.Collections.Generic.List`1[[{0}, {1}]]", alternateTypeName, alternateAssembly));
            else
                return Type.GetType(String.Format("{0}, {1}", alternateTypeName, alternateAssembly));
        }
        else
            return null; // this seems to do the trick for binary serialization, but i'm not sure if it is supposed to work
    }
}

person Mark    schedule 29.10.2013    source источник


Ответы (3)


Это может сработать (вместо вашего переопределения).

public override Type BindToType(string assemblyName, string typeName)
        {
            var m = Regex.Match(typeName, @"^(?<gen>[^\[]+)\[\[(?<type>[^\]]*)\](,\[(?<type>[^\]]*)\])*\]$");
            if (m.Success)
            { // generic type
                var gen = GetFlatTypeMapping(m.Groups["gen"].Value);
                var genArgs = m.Groups["type"]
                    .Captures
                    .Cast<Capture>()
                    .Select(c =>
                        {
                            var m2 = Regex.Match(c.Value, @"^(?<tname>.*)(?<aname>(,[^,]+){4})$");
                            return BindToType(m2.Groups["aname"].Value.Substring(1).Trim(), m2.Groups["tname"].Value.Trim());
                        })
                    .ToArray();
                return gen.MakeGenericType(genArgs);
            }
            return GetFlatTypeMapping(assemblyName,typeName);
        }

Тогда вам просто нужно реализовать по-своему функцию GetFlatTypeMapping (не беспокоясь об общих аргументах).

Что вам нужно будет сделать, так это вернуть typeof(List<>) и typeof(Dictionary<,>) (или любой другой общий код, который вы хотели бы использовать), когда его спросят.

NB: Я сказал typeof(List<>) ! не typeof(List<something>)... это важно.

Отказ от ответственности: из-за регулярного выражения "(?[^]]*)" этот фрагмент не поддерживает вложенные универсальные типы, такие как List<List<string>> ... вам придется немного настроить его, чтобы поддерживать его!

person Olivier    schedule 12.11.2013

Хотя это может не ответить на ваш вопрос, это может решить вашу проблему:

Я смог сериализовать все сборки, кроме одной, необходимые для десериализации, вместе со структурой объекта. Хитрость заключается в том, чтобы использовать отражение для проверки структуры вашего объекта на наличие типов и сборок, в которых они определены. Затем вы можете записать сборки как двоичные данные в объект при сериализации и загрузить их в AppDomain, где вы можете использовать их для десериализации остальных структуры объекта.

Вы должны поместить обработку AppDomain, корневой объект и некоторые базовые классы в сборку, которая не будет меняться, а все остальное определяется в сборках в зависимости от этого «якоря».

Плюсы:

  • сериализация не прерывается до тех пор, пока сборка якоря не изменяется
  • может использоваться для запутывания, например. некоторая необходимая сборка может быть скрыта в любом файле
  • может использоваться для онлайн-обновлений, например. отправить сборку с любыми данными
  • насколько я понимаю, с дженериками проблем нет

Недостатки:

  • проблемы с безопасностью, так как начальная загрузка может использоваться для внедрения кода (некоторая дополнительная работа для сборки SecurityEvidences)
  • Границы AppDomain нужно пересечь
  • взрывает ваши сериализованные данные (хорошо, для файлов - плохо, для связи)

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

Извините, я не могу предоставить код, так как он слишком сложный и специфичный. Но я делюсь некоторыми мыслями в этих других моих ответах:

разница между атрибутом DataContract и атрибутом Serializable в .net

Необходимо подключить событие AssemblyResolve, когда DisallowApplicationBaseProbing = true

person No answer    schedule 07.11.2013

Является ли это жестким требованием, чтобы вы ДОЛЖНЫ использовать BinaryFormatter для своей десериализации?

Если для вас не является жестким требованием использовать BinaryFormatter для сериализации, рассмотрите альтернативные методы сериализации, такие как JSON.Net или ProtoBuf.Net. Любой из них создаст сериализацию ваших данных, независимую от платформы и версии.

В качестве альтернативы вы можете выполнить двоичную сериализацию самостоятельно (что быстрее и меньше, чем BinaryFormatter, но, как правило, требует больше кода, поскольку вы должны быть уверены, что сериализатор и десериализатор написаны практически идентично друг другу.

Если вы должны использовать BinaryFormatter, обязательно создайте его с помощью FormatterAssemblyStyle.Simple, чтобы обойти проблемы с версиями. Это говорит ему не выполнять педантическую проверку версии сборки.

person dodexahedron    schedule 05.11.2013
comment
Я заявил, что требуется использовать BinaryFormatter - person Mark; 05.11.2013
comment
На самом деле, вы этого не сделали (по крайней мере, не ясно) и явно спросили, есть ли что-то, что делает то, что вы хотите (конечным результатом является сериализация с обратной совместимостью, не так ли?), и это было бы безответственно с вашей стороны как разработчик и я как человек, пытающийся помочь не рассматривать альтернативы, когда это необходимо. Я предоставил альтернативные решения, а также дал вам кое-что попробовать, используя BinaryFormatter. Вы указали FormatterAssemblyStyle.Simple для своего BinaryFormatter? - person dodexahedron; 08.11.2013
comment
Я не понимаю, как изменение методов сериализации в середине потока обеспечит обратную совместимость? - person Casey; 11.06.2014