How to create a SerializationBinder for the Binary Formatter that handles the moving of types from one assembly and namespace to another
Asked Answered
F

3

9

The context is as follows

  1. I want to refactor code by moving it around to different projects
  2. Some of this code comprises of serializable DTOs that are used to send and receive data across multiple endpoints
  3. If I move the code around, serialization breaks (therefore it is not backward compatible with older versions of my application)

A solution to this problem is the SerializationBinder which allows me to "redirect" in a sense from one type to another.

I therefore want to create a SerializationBinder to meet this need. However it must do so by meeting the following requirements

  1. The inputs to the SerializationBinder should be a list of old type to new type mappings. The mapping should include the old assembly name (no version, no public key token) and the old full name of the type (namespace and name) as well as the new assembly name and new full name of the type
  2. For the types that are in the inputs, version numbers of assemblies should be ignored
  3. It should handle generics if my types happen to be in generics (List, Dictionary, etc) without needing to include the generics in the inputs
  4. For anything that is not in the inputs (i.e. types that have not moved or .NET types like dataset for example) it should default to using the out of the box algorithm of the binary serializer

Is this possible or am I dreaming? Is there something out there that already does this? I would assume this is a common problem.

So far I see no easy way of doing 3 and no way at all of doing 4.

Here is an attempt

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
    }
}
Futile answered 29/10, 2013 at 18:37 Comment(0)
C
6

This could work (instead of your override).

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

Then you just have to implement your way the function GetFlatTypeMapping (not worrying of about generic arguments).

What you will have to do is to return typeof(List<>) and typeof(Dictionary<,>) (or any other generic you would like to use) when asked.

nb: I said typeof(List<>) ! not typeof(List<something>) ... that's important.

disclaimer: because of the regex "(?[^]]*)", this snipped does not support nested generic types like List<List<string>> ... you will have to tweak it a bit to support it !

Crambo answered 12/11, 2013 at 11:6 Comment(0)
P
0

Although it might not answer your question this might solve your problem:

I was able to serialize all but one assembly required for deserialization alongside an object structure. The trick is to use reflection to inspect your object structure for types and the assemblies they are defined in. You can then write the assemblies as binary data into an object on serialization and load them into an AppDomain where you can use them to deserialize the rest of the object structure.

You have to put the AppDomain handling, the root object and some base classes into an assembly that won't change and everything else is defined in assemblies depending on this "anchor".

Upsides:

  • serialization does not break as long as the anchor assembly does not change
  • can be used for obfuscation, e.g. some required assembly can be hidden in any file
  • can be used for online updates, e.g. ship the assembly with any data
  • as far as I'm concerned no problem with generics

Downsides:

  • security issues as bootstrapping could be used to inject code (some extra work for assembly SecurityEvidences)
  • AppDomain borders are some work to cross
  • blows up your serialized data (ok, for files - bad, for communication)

But hey, communication does not break unless you have different client versions and then you can do the bootstrapping on handshake.

Sorry, I can't provide the code as it's too complex and too specific. But I share some insights in these other answers of mine:

difference between DataContract attribute and Serializable attribute in .net

Need to hookup AssemblyResolve event when DisallowApplicationBaseProbing = true

Pistoia answered 7/11, 2013 at 17:3 Comment(0)
O
-2

Is it a hard requirement that you MUST be using the BinaryFormatter for your de/serialization?

If it's not a hard requirement for you to be using the BinaryFormatter to do your serialization, consider alternative serialization methods, such as JSON.Net or ProtoBuf.Net. Either one of these will create platform- and version-independent serializations of your data.

Alternatively, you can perform binary serialization yourself (which is both faster and smaller than the BinaryFormatter, but generally more code-intensive as you have to be sure to write the serializer and deserializer essentially identically to each other.

If you must use BinaryFormatter, be sure to construct it with FormatterAssemblyStyle.Simple, to get around versioning problems. That tells it to not do the pedantic check of the assembly version.

Oval answered 5/11, 2013 at 18:52 Comment(3)
I've stated that the requirement is to use the BinaryFormatterFutile
Actually, you didn't (at least not clearly), and explicitly asked if there's something out there that does what you want (the end-result being backward-compatible serialization, no?), and it would be irresponsible of you as a developer and me as someone trying to help to not consider alternatives, when necessary. I both provided alternative solutions as well as gave you something to try, using the BinaryFormatter. Have you specified FormatterAssemblyStyle.Simple for your BinaryFormatter?Oval
I can't see how changing serialization methods mid-stream would allow backwards compatibility?Tomokotomorrow

© 2022 - 2024 — McMap. All rights reserved.