The Missing .NET #6: Version Tolerant Serialization

The .NET framework is huge, but not so huge that it does everything for everyone; there are things that they in Redmond miss or don’t do for whatever reason but is still generally applicable to many developers. So, dear reader, I present to you a series of posts on stuff I find missing in .NET, typically where even the Google fails to find the answer. It could be a useful class, a technique, a good practice or documentation that should be in the framework but isn’t or isn’t widely publicized. Click here for a complete list of Missing .NET entries.


I work on a .NET application that now runs on 2.0 but was written for 1.1. It’s bit like a farmer’s son who moved to the city and still does things as if he was on the farm. Like mow his two-square-meter lawn with a ride-on mower, "just like daddy used to back home." That’s fine, it gets the job done, but, dude, there’s a better way. Get a push mower, damn it.

Getting it back in the garage is a real bitch

Getting it back in the garage is a real bitch

In .NET 1.1, serialization (binary serialization, to be clear) is a bit hairy when you’re successful enough to ship multiple versions of your product and you don’t consider versioning until the first bug report about serialization problems. Changing the type, by either adding or removing fields, in .NET 1.1 causes previous versions of your type to lose their mind. The only way to deal with it at all was to implement ISerializable, which is fraught with problems, not the least of which is it makes you entirely responsible for serialization and deserialization of the whole type for ever and ever.

Now that we’re running on .NET 2.0, we have more options. Microsoft changed the behaviour so that it’s version tolerant by default – no more exceptions on new fields. But there is other awesome stuff besides, so we can have more control over the whole process.

Suppose we had the following serializable type:

//Version 1
[Serializable]
public class MyClass
{
  private string myString;

  public string MyString
  {
     get { return myString; }
     set { myString = value; }
  }
}

You ship your software. MyClass is used to great success; in fact, it made you the most successful MyClass maker in the world. For version 2, you’ve promised big things for your customers. After all the design work your new, improved MyClass becomes the following:

//Version 2
[Serializable]
public class MyClass
{
  private string myString;
  private int myInt;

  public string MyString
  {
     get { return myString; }
     set { myString = value; }
  }

  public int MyInt
  {
     get { return myInt; }
     set { myInt = value; }
  }
}

Oooh, look at all the new features!

MyClass Maker version 2 gets in to QA to test upgrading. All the MyClass version 1 instances upgrade just fine, but you had the good fortune to run on .NET 2.0, which by default doesn’t throw exceptions when there is a field for which the serialized instance does not have data. It just works; Microsoft put serialization in the Pit of Success in .NET 2.0.

But QA came back to Dev with the complaint that after upgrading the MyClass instance didn’t show off the awesomeness of the new features. They think the default value of the new field should be something else. Since you’ve just been reading up on Version Tolerant Serialization, you know just what to do.

What we need is a way to influence deserialization. With .NET 2.0, Microsoft introduced four attributes that deal with serialization process: OnSerializingAttribute, OnSerializedAttribute, OnDeserializingAttribute, OnDeserializedAttribute.

To set a default value for myInt, we create a method with a specific signature and apply the OnDeserializingAttribute to it You could also set the default where you declare the variable if all new instances have the same default, like so:

[OnDeserializing]
private void SetMyIntDefault(StreamingContext sc)
{
   myInt = 42;
}

If you implement ISerializable as well, this method is called before the deserialization constructor. Only one method per class can be marked with each of these attributes. We set the default value in the OnDeserializing method so that, if there is a myInt value in the serialized instance, the default value gets overwritten with the serialized value.

Your marketer just came back from a conference. He really likes this social aspect to these new-fangled web 2.0 applications. Now you’re required to enable sharing of MyClass instances among users. Trouble is, we can’t guarantee everyone is using the most up-to-date version and marketing won’t let us force upgrades. It’s OK, we can deal with that. We just have to tweak our new MyClass version; we’ll mark the myInt field declaration with the OptionalFieldAttribute, so that the serializer will know not to explode if it’s not there. I’m not sure whether this is necessary because during my testing for this article, the presence or absence of OptionalField didn’t seem to matter. But it could be because I was lazy: I didn’t test from 1.1 to 2.0.

[Serializable]
public class MyClass
{
   private string myString;
   [OptionalField(VersionAdded=2)]
   private int myInt;

   public string MyString
   {
      get { return myString; }
      set { myString = value; }
   }

   public int MyInt
   {
      get { return myInt; }
      set { myInt = value; }
   }

   [OnDeserializing]
   private void SetMyIntDefault(StreamingContext sc)
   {
      myInt = 42;
   }
}

Because I’m a good little developer that reads documentation, I included the VersionAdded property in my OptionalField declaration. This is for "future compatibility with new serializers in the framework." I’m not sure there will be other serializers in the framework since the CLR hasn’t changed since 2.0, as well as the whole confusing red bits/green bits thing. But you know what assuming does, right? So I put it in.

One thing I tried in my experimentation was the following: suppose you have a ton of types that implement ISerializable because they’re legacy 1.1 types. As long as you keep the convention of your keys being the same as the name of the field, then you can move the ISerializable implementation to the dustbin. It seems to just work, although I’d test it a little more thoroughly. Suppose MyClass version 1 looked like the below code. In Version 2, you could delete the ISerializable stuff.

[Serializable]
public class MyClass : ISerializable
{
   private string myString;

   public MyClass()
   {
   }

   protected MyClass(SerializationInfo info, StreamingContext context)
   {
      myString = info.GetString("myString");
   }

   public string MyString
   {
      get { return myString; }
      set { myString = value; }
   }

   public void GetObjectData(SerializationInfo info, StreamingContext context)
   {
      info.AddValue("myString", myString);
   }
}

Deleting code you don’t need anymore feels so good. Release yourself from the bonds of ISerializable!

There are so many unsung improvements in the BCL in .NET 2.0 that I’m still finding them nearly four years later. The above improvements to serialization move it from cumbersome chore fraught with peril to those who dare master it to the magic of "it just works." Serialization code is hard to get right, so I’m glad Microsoft took most of the onerous stuff off our hands. I’d encourage you to take a look at the MSDN article on Version Tolerant Serialization for all the details on this stuff. I’ll leave this article with their recommended practices for creating serializable types.

To ensure proper versioning behavior, follow these rules when modifying a type from version to version:

  • Never remove a serialized field.

  • Never apply the NonSerializedAttribute attribute to a field if the attribute was not applied to the field in the previous version.

  • Never change the name or the type of a serialized field.

  • When adding a new serialized field, apply the OptionalFieldAttribute attribute.

  • When removing a NonSerializedAttribute attribute from a field (that was not serializable in a previous version), apply the OptionalFieldAttribute attribute.

  • For all optional fields, set meaningful defaults using the serialization callbacks unless 0 or null as defaults are acceptable.

To ensure that a type will be compatible with future serialization engines, follow these guidelines:

  • Always set the VersionAdded property on the OptionalFieldAttribute attribute correctly.

  • Avoid branched versioning.

Well, actually, let me leave by adding one more: Don’t implement ISerializable when introducing new types on a .NET 2.0 or greater project. If you can’t serialize your types with the above attributes, you’re doing it wrong.

2 Replies to “The Missing .NET #6: Version Tolerant Serialization”

Comments are closed.