Serialization Part 2: Version-Tolerant Serialization

A prior article I wrote, titled "Serialization/Deserialization in .NET," introduced serialization and touched on some of the basics. This article assumes you are familiar with the material that article covered. If you aren't, refer to the prior article first.

As any good sequel should, this follow-up builds on the momentum of the first, yet delivers a different message. This article focuses on serialization and deserialization in the upcoming 2.0 release of the Microsoft .NET Framework, specifically version-tolerant serialization.

Version-Tolerant Serialization (VTS)

Versioning can mean a number of different things in software development, but commonly it is used synonymously with the term release and means the next iteration of a particular software product. If you think in terms of the objects used in your code, it could mean a change to a customer or some other object. Each time you release a new version of your software, whether it is an actual software product, a business application, or a pet application of your own, you have the potential to run into versioning issues if you're using serialization within your application.

Serialization Example

To demonstrate the problem, the following example defines an object and then serializes it to a file. The object will be modeled after a couple of the fields in the Customer table in the sample Northwind database that comes with Microsoft SQL Server.

Here is a definition for the Customer, followed by code to serialize it to and then read it back from the file and display it:

using System.Runtime.Serialization;

[Serializable]
class Customer
{
   private string companyName = "";
   public string CompanyName
   {
      get { return this.companyName; }
      set { this.companyName = value; }
   }

   private string contactName = "";
   public string ContactName
   {
      get { return this.contactName; }
      set { this.contactName = value; }
   }

   public void Clear()
   {
      this.CompanyName = "";
      this.ContactName = "";
   }
}

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Formatters;

class Program
{
   static void Main(string[] args)
   {
      Customer customer = new Customer();
      customer.CompanyName = "Alfreds Futterkiste";
      customer.ContactName = "Maria Anders";

      using (FileStream fileStream =
             new FileStream(@"C:\\mytest.bin", FileMode.Create))
      {
         BinaryFormatter formatter = new BinaryFormatter();
         formatter.Serialize(fileStream, customer);
      }
      customer.Clear();
      using (FileStream fileStream =
             new FileStream(@"C:\\mytest.bin", FileMode.Open))
      {
         BinaryFormatter formatter = new BinaryFormatter();
         customer = (Customer)formatter.Deserialize(fileStream);
      }
      Console.Write("{0} - {1}", customer.CompanyName,
                    customer.ContactName);
}

When you execute the code above, it creates an instance of your Customer, sets the name and company name properties, saves it to a file, and then reads it back in and displays it. It's a rudimentary example that demonstrates serialization and deserialization and creates a binary data file. You could just as easily store your serialized object in a database rather than a file, but this example keeps it simple.

Now, check out what happens when you expand your Customer definition to include an additional ContactTitle property:

[Serializable]
class Customer
{
   private string companyName = "";
   public string CompanyName
   {
      get { return this.companyName; }
      set { this.companyName = value; }
   }

   private string contactName = "";
   public string ContactName
   {
      get { return this.contactName; }
      set { this.contactName = value; }
   }

   private string contactTitle = "";
   public string ContactTitle
   {
      get { return this.contactTitle; }
      set { this.contactTitle = value; }
   }

   public void Clear()
   {
      this.CompanyName = "";
      this.ContactName = "";
      this.ContactTitle = "";
   }
}

If you try to read the contents of that same file you saved out before, you'll get a nasty outcome in the form of an exception (see Figure 1):

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Formatters;

class Program
{
   static void Main(string[] args)
   {
      Customer customer = new Customer();

      using (FileStream fileStream =
             new FileStream(@"C:\\mytest.bin", FileMode.Open))
      {
         BinaryFormatter formatter = new BinaryFormatter();
         customer = (Customer)formatter.Deserialize(fileStream);
      }
      Console.Write("{0} - {1}", customer.CompanyName,
                    customer.ContactName);
}

Figure 1: SerializationException Is Thrown

The reason for this exception is that you "versioned" your Customer object. The previously saved binary file didn't have all of the same properties as the new format, so deserializing the object from the file fails with an exception as depicted in Figure 1.

There are a couple of ways to work around the versioning problem:

  1. Take full control of the serialization/deserialization process by implementing the ISerializable interface.
  2. Use the VTS attributes to control the serialization/deserialization.

The ISerializable interface requires more code and control than you may have actually wanted. The middle of the road solution is to use the new VTS attributes.

Serialization Part 2: Version-Tolerant Serialization

VTS Attributes

A couple of simple attributes that help control serialization will help you with the problem above:

  • NonSerializedAttribute—Ignore the field altogether
  • OptionalFieldAttribute—Ignore the field if not present

One simple way to work around the versioning issue would be to use the NonSerialized attribute. If you placed the attribute on your contactTitle field, it would be ignored when you try to deserialize the Customer from the prior binary file.

The following is a modified code snippet for the Customer object:

[NonSerialized()]
private string contactTitle = "";
public string ContactTitle
{
   get { return this.contactTitle; }
   set { this.contactTitle = value; }
}

That is all well and good as long as you have no intention of using the new property in newly serialized versions of your object. It does not help in the case where you want to serialize/deserialize both versions of your object, meaning use the ContactTitle if it's there and ignore if it's not. You can use the OptionalFieldAttribute instead.

The following is a modified code snippet for the Customer object:

[OptionalField()]
private string contactTitle = "";
public string ContactTitle
{
   get { return this.contactTitle; }
   set { this.contactTitle = value; }
}

This is a modified code snippet for your testing program:

using (FileStream fileStream = new FileStream(@"C:\\mytest.bin",
                                              FileMode.Open))
{
   BinaryFormatter formatter = new BinaryFormatter();
   customer = (Customer)formatter.Deserialize(fileStream);
}

if (customer.ContactTitle ==
    null || customer.ContactTitle.Length == 0 )
{
   Console.Write("{0} - {1}", customer.CompanyName,
                 customer.ContactName);
}
else
{
   Console.Write("{0} - {1} - {2}", customer.CompanyName,
                 customer.ContactName, customer.ContactTitle);
}

Now, if you deserialize your Customer using the new version, it will ignore the contactTitle because it is not available in the previously created file. You can verify that it will indeed read the ContactTitle (if it is present) by creating another serialized version that does have it set, and read it back in using the same code above. My code to create a new serialized version of the test file looked something like this:

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization.Formatters;

class Program
{
   static void Main(string[] args)
   {
      Customer customer = new Customer();
      customer.CompanyName = "Alfreds Futterkiste";
      customer.ContactName = "Maria Anders";
      customer.ContactTitle = "Sales Representative";

      using (FileStream fileStream =
             new FileStream(@"C:\\mytest-v2.bin", FileMode.Create))
      {
         BinaryFormatter formatter = new BinaryFormatter();
         formatter.Serialize(fileStream, customer);
      }
      customer.Clear();
      using (FileStream fileStream =
             new FileStream(@"C:\\mytest-v2.bin", FileMode.Open))
      {
         BinaryFormatter formatter = new BinaryFormatter();
         customer = (Customer)formatter.Deserialize(fileStream);
      }

      if (customer.ContactTitle ==
          null || customer.ContactTitle.Length == 0 )
      {
         Console.Write("{0} - {1}",
                       customer.CompanyName, customer.ContactName);
      }
      else
      {
         Console.Write("{0} - {1} - {2}", customer.CompanyName,
         customer.ContactName, customer.ContactTitle);
      }
}

By commenting out the file-creation part and changing the read part between mytest.bin and mytest-v2.bin, you should be able to prove that you can read both versions into the new Customer object.

More Control of the Serialization Process

In the past, the only way to get full control of the serialization process was to implement the ISerializable interface, which equates to more code and more complexity. Additional attributes are available to allow you more control without having to go as far as implementing the ISerializable interface. Each of those attributes can be placed on a method and has the following impact:

  • OnDeserializingAttribute—Called during deserialization
  • OnDeserializedAttribute—Called immediately after deserialization is complete
  • OnSerializingAttribute—Called during serialization
  • OnSerializedAttribute—Called immediately after serialization is complete

This offers another middle-of-the-road solution to provide even more control without requiring you to go to the full extent of implementing the ISerializable interface.

The following is an additional code snippet for the Customer object:

[OnDeserializing()]
void OnDeserializingTest(StreamingContext context)
{
   // Do something here during deserialization...
}

[OnDeserialized()]
void OnDeserializedTest(StreamingContext context)
{
   // Do something here after deserialization...
}

[OnSerializing()]
void OnSerializingTest(StreamingContext context)
{
   // Do something here during serialization...
}

[OnSerialized()]
void OnSerializedTest(StreamingContext context)
{
   // Do something here after serialization...
}

A Big Improvement in Serialization

You now have seen an introduction to the version-tolerant serialization that is in the pending release of the 2.0 version of the Microsoft .NET Framework. This is a big improvement in serialization, especially for those who want some control of the process but don't want to have to fully implement ISerializable and all of the baggage that it carries with it such as extra code.

Future Columns

The topic of the next column is yet to be determined. If you have something in particular that you would like to see explained here, you could reach me at mstrawmyer@crowechizek.com.



About the Author

Mark Strawmyer

Mark Strawmyer is a Senior Architect of .NET applications for large and mid-size organizations. He specializes in architecture, design and development of Microsoft-based solutions. Mark was honored to be named a Microsoft MVP for application development with C# for the fifth year in a row. You can reach Mark at mark.strawmyer@crowehorwath.com.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • VMware vCloud® Government Service provided by Carpathia® is an enterprise-class hybrid cloud service that delivers the tried and tested VMware capabilities widely used by government organizations today, with the added security and compliance assurance of FedRAMP authorization. The hybrid cloud is becoming more and more prevalent – in fact, nearly three-fourths of large enterprises expect to have hybrid deployments by 2015, according to a recent Gartner analyst report. Learn about the benefits of …

Most Popular Programming Stories

More for Developers

RSS Feeds