Deserialization considerations when renaming objects, namespaces, or assemblies

Posted on Posted in Uncategorized

In one of my cur­rent projects, we are con­sid­er­ing refac­tor­ing some lega­cy code into new assem­blies and name­spaces. The prod­uct is an enter­prise back­up solu­tion and the objects we're mov­ing are seri­al­ized to disk dur­ing a back­up. To restore, the objects are dese­ri­al­ized from disk and read in for processing.

No types like that around here

The issue we ran into almost imme­di­ate­ly was sup­port­ing lega­cy back­ups. We want­ed cus­tomers to still be able to use their exist­ing back­ups to restore, even if they upgrad­ed to the new prod­uct. How­ev­er, mov­ing objects into new name­spaces or assem­blies breaks deserialization.

Take, as an exam­ple, an Objects assem­bly which con­tains a Square class in the Bluey.Objects name­space. When a Square is seri­al­ized to disk, both the assem­bly Full­Name:
  Objects, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
and the object's type name (includ­ing name­space):
  Bluey.Objects.Square
are saved to the byte stream.

If you lat­er decide to rename the Objects assem­bly to Shapes, any objects that were pre­vi­ous­ly seri­al­ized will fail to dese­ri­al­ize because the Objects assem­bly no longer exists and can­not be loaded. Like­wise, if you were to leave the assem­bly name alone and just rename the Bluey.Objects name­space to Bluey.Shapes, the object would still fail to dese­ri­al­ize because Bluey.Objects.Square no longer exists.

Enter Seri­al­iza­tion­Binder

This prob­lem can be solved by inject­ing cus­tom Seri­al­iza­tion­Binder log­ic into the for­mat­ter used to dese­ri­al­ize the objects. The Seri­al­iza­tion­Binder is respon­si­ble for tak­ing a type name and assem­bly name and bind­ing it to the cor­re­spond­ing type. By inject­ing our own binder, we can over­ride the default behav­ior and han­dle spe­cial cas­es. Here is a very sim­plis­tic exam­ple binder we can use for the Objects->Shapes rename sce­nario above:

  pub­lic class Object­sToShapes­Binder : Seri­al­iza­tion­Binder {
    pub­lic over­ride Type Bind­To­Type(string assem­bly­Name, string type­Name) {
      if (type­Name == "Bluey.Objects.Square") {
        type­Name = "Bluey.Shapes.Square";
      }
      return Type.GetType(typeName);
    }
  }

To use this cus­tom binder, we sim­ply attach it to the for­mat­ter we're going to use for deserialization:

  var bin­For­mat­ter = new Bina­ry­For­mat­ter();
  binFormatter.Binder = new Object­sToShapes­Binder();

At this point, any dese­ri­al­iza­tion done with bin­For­mat­ter will use our binder to deter­mine the rep­re­sen­ta­tive Types to cre­ate. Because the assem­bly is like­ly to be already loaded in mem­o­ry at run­time, we do not need to wor­ry about the assem­bly name para­me­ter in our cus­tom Bind­To­Type method. How­ev­er, if that were not the case, we could use the assem­bly name to deter­mine the cor­rect assem­bly to load before try­ing to Type.GetType.

The use of the cus­tom binder applies to any object in the object graph, not just the root object being dese­ri­al­ized. So dese­ri­al­iz­ing a Hashtable con­tain­ing a Square will work just as well as dese­ri­al­iz­ing a lone Square.

Leave a Reply

Your email address will not be published. Required fields are marked *