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

Контекст выглядит следующим образом

  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
    }
}

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<,>) (или любой другой универсальный, который вы хотели бы использовать), когда спросят.

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

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

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

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

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

расквитаться:

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

Недостатки:

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам