Deserialization considerations when renaming objects, namespaces, or assemblies

In one of my current projects, we are considering refactoring some legacy code into new assemblies and namespaces. The product is an enterprise backup solution and the objects we’re moving are serialized to disk during a backup. To restore, the objects are deserialized from disk and read in for processing.

No types like that around here

The issue we ran into almost immediately was supporting legacy backups. We wanted customers to still be able to use their existing backups to restore, even if they upgraded to the new product. However, moving objects into new namespaces or assemblies breaks deserialization.

Take, as an example, an Objects assembly which contains a Square class in the Bluey.Objects namespace. When a Square is serialized to disk, both the assembly FullName:
  Objects, Version=, Culture=neutral, PublicKeyToken=null
and the object’s type name (including namespace):
are saved to the byte stream.

If you later decide to rename the Objects assembly to Shapes, any objects that were previously serialized will fail to deserialize because the Objects assembly no longer exists and cannot be loaded. Likewise, if you were to leave the assembly name alone and just rename the Bluey.Objects namespace to Bluey.Shapes, the object would still fail to deserialize because Bluey.Objects.Square no longer exists.

Enter SerializationBinder

This problem can be solved by injecting custom SerializationBinder logic into the formatter used to deserialize the objects. The SerializationBinder is responsible for taking a type name and assembly name and binding it to the corresponding type. By injecting our own binder, we can override the default behavior and handle special cases. Here is a very simplistic example binder we can use for the Objects->Shapes rename scenario above:

  public class ObjectsToShapesBinder : SerializationBinder {
    public override Type BindToType(string assemblyName, string typeName) {
      if (typeName == “Bluey.Objects.Square“) {
        typeName = “Bluey.Shapes.Square“;
      return Type.GetType(typeName);

To use this custom binder, we simply attach it to the formatter we’re going to use for deserialization:

  var binFormatter = new BinaryFormatter();
  binFormatter.Binder = new ObjectsToShapesBinder();

At this point, any deserialization done with binFormatter will use our binder to determine the representative Types to create. Because the assembly is likely to be already loaded in memory at runtime, we do not need to worry about the assembly name parameter in our custom BindToType method. However, if that were not the case, we could use the assembly name to determine the correct assembly to load before trying to Type.GetType.

The use of the custom binder applies to any object in the object graph, not just the root object being deserialized. So deserializing a Hashtable containing a Square will work just as well as deserializing a lone Square.

Leave a Reply

Your email address will not be published.