GOTCHA 17 Versioning may lead to Serialization headaches


GOTCHA #17 Versioning may lead to Serialization headaches

Serialization is a mechanism that allows converting an object tree into a series of bytes. The most effective use of serialization is in remoting, where objects are passed by value between AppDomains or between applications. Another use of serialization is to store a snapshot of your object tree into a storage medium like a file. You can get a minimum level of support by just marking the type with a Serializable attribute. This is quite powerful, and almost effortless to implement.

If a type is flagged as Serializable, it indicates to the Serializer that an instance of the class may be serialized. If any type in the graph of the object being serialized is not Serializable, the CLR will throw a SerializationException. All fields in your type are serialized except those you mark with the NonSerialized attribute.

However, there are problems. The biggest problem with serializing to a file is versioning. If the version of a class changes after one of its objects is serialized, then the object can't be deserialized. One way to get around this limitation is to implement the ISerializable interface.

When a class implements ISerializable, the ISerializable.GetObjectData() method is invoked during the serialization process. A special constructor with the same signature as GetObjectData() is called during deserialization. Both these methods are passed a SerializationInfo object. Think of this object as a data bag or a hash table. During serialization, you can store key-value pairs into the SerializationInfo object. When you deserialize the object, you can ask for these values using their keys.

So how do you deal with versioning? If you remove a field from the class, just don't ask for its value during deserialization. But what if you add a field to the class? When you ask for that field during deserialization, an exception is thrown if the object being deserialized is from an older version. Consider Examples 2-14 and 2-15.

Example 2-14. A serialization example (C#)

C# (ReflectionToSerialize)

 //Engine.cs using System; namespace Serialization {     [Serializable]     public class Engine     {         private int power;         public Engine(int thePower)         {             power = thePower;         }         public override string ToString()         {             return power.ToString();         }     } } //Car.cs using System; using System.Runtime.Serialization; namespace Serialization {     [Serializable]     public class Car : ISerializable      {         private int yearOfMake;         private Engine theEngine;         public Car(int year, Engine anEngine)         {             yearOfMake = year;             theEngine = anEngine;         }         public override string ToString()         {             return yearOfMake + ":" + theEngine;         }         #region ISerializable Members         public Car(SerializationInfo info,             StreamingContext context)         {             yearOfMake = info.GetInt32("yearOfMake");             theEngine = info.GetValue("theEngine",                     typeof(Engine)) as Engine;         }         public void GetObjectData(SerializationInfo info,             StreamingContext context)         {             info.AddValue("yearOfMake", yearOfMake);             info.AddValue("theEngine", theEngine);         }         #endregion     } } //Test.cs using System; using System.IO; using System.Runtime.Serialization.Formatters.Binary; namespace Serialization {     class Test     {         [STAThread]         static void Main(string[] args)         {             Console.WriteLine(                 "Enter s to serialize, d to deserialize");             string input = Console.ReadLine();             if (input.ToUpper() == "S")             {                 Car aCar = new Car(2004, new Engine(500));                 Console.WriteLine("Serializing " + aCar);                 FileStream strm = new FileStream("output.dat",                     FileMode.Create, FileAccess.Write);                 BinaryFormatter formatter =                     new BinaryFormatter();                 formatter.Serialize(strm, aCar);                 strm.Close();             }             else             {                 FileStream strm = new FileStream("output.dat",                     FileMode.Open, FileAccess.Read);                 BinaryFormatter formatter                     = new BinaryFormatter();                 Car aCar = formatter.Deserialize(strm) as Car;                 strm.Close();                 Console.WriteLine("DeSerialized " + aCar);             }         }     } } 

Example 2-15. A serialization example (VB.NET)

VB.NET (ReflectionToSerialize)

 'Engine.vb <Serializable()> _ Public Class Engine     Private power As Integer     Public Sub New(ByVal thePower As Integer)         power = thePower     End Sub     Public Overrides Function ToString() As String         Return power.ToString()     End Function End Class 'Car.vb Imports System.Runtime.Serialization <Serializable()> _ Public Class Car     Implements ISerializable     Private yearOfMake As Integer     Private theEngine As Engine     Public Sub New(ByVal year As Integer, ByVal anEngine As Engine)         yearOfMake = year         theEngine = anEngine     End Sub     Public Overrides Function ToString() As String         Return yearOfMake & ":" & theEngine.ToString()     End Function     Public Sub New( _         ByVal info As SerializationInfo, _         ByVal context As StreamingContext)         yearOfMake = info.GetInt32("yearOfMake")         theEngine = CType(info.GetValue("theEngine", _              GetType(Engine)), Engine)     End Sub     Public Sub GetObjectData(ByVal info As SerializationInfo, _         ByVal context As StreamingContext) _             Implements ISerializable.GetObjectData         info.AddValue("yearOfMake", yearOfMake)         info.AddValue("theEngine", theEngine)     End Sub End Class 'Test.vb Imports System.IO Imports System.Runtime.Serialization.Formatters.Binary Module Test     Public Sub Main()         Console.WriteLine( _             "Enter s to serialize, d to deserialize")         Dim input As String = Console.ReadLine()         If input.ToUpper() = "S" Then             Dim aCar As Car = New Car(2004, New Engine(500))             Console.WriteLine("Serializing " & aCar.ToString())             Dim strm As FileStream = New FileStream("output.dat", _                  FileMode.Create, FileAccess.Write)             Dim formatter As New BinaryFormatter             formatter.Serialize(strm, aCar)             strm.Close()         Else             Dim strm As FileStream = New FileStream("output.dat", _                  FileMode.Open, FileAccess.Read)             Dim formatter As New BinaryFormatter             Dim aCar As Car = CType(formatter.Deserialize(strm), Car)             strm.Close()             Console.WriteLine("DeSerialized " & aCar.ToString())         End If     End Sub End Module 

In the previous code, you either serialize or deserialize a Car object. The code is pretty straightforward so far. Now, let's run it once to serialize the Car object and then run it again to deserialize it. You get the outputs shown in Figures 2-13 and Figures 2-14.

Figure 2-13. Output from Example 2-14: Serializing


Figure 2-14. Output from Example 2-14: Deserializing


Now let's modify the Car class by adding a miles field. How can you handle this during deserialization from an older version? If you simply write the code to fetch the miles field within the special constructor you will get an exception; the deserialization will fail because the field is missing. Now, how can you process this in your code? One way is to just handle the exception and move on.

The Car class with the required code change is shown in Example 2-16.

Example 2-16. Handling an exception during deserialization

C# (ReflectionToSerialize)

 //Car.cs using System; using System.Runtime.Serialization; namespace Serialization {     [Serializable]     public class Car : ISerializable      {         private int yearOfMake;         private Engine theEngine;          private int miles = 0;         public Car(int year, Engine anEngine)         {             yearOfMake = year;             theEngine = anEngine;         }         public override string ToString()         {              return yearOfMake + ":" + miles + ":" + theEngine;         }         #region ISerializable Members         public Car(SerializationInfo info,             StreamingContext context)         {             yearOfMake = info.GetInt32("yearOfMake");             theEngine = info.GetValue("theEngine",                 typeof(Engine)) as Engine;              try             {                 miles = info.GetInt32("miles");             }             catch(Exception)             {                 //Shhhhh, let's move on quietly.             }         }         public void GetObjectData(SerializationInfo info,             StreamingContext context)         {             info.AddValue("yearOfMake", yearOfMake);             info.AddValue("theEngine", theEngine);              info.AddValue("miles", miles);         }         #endregion     } } 

VB.NET (ReflectionToSerialize)

 'Car.vb Imports System.Runtime.Serialization <Serializable()> _ Public Class Car     Implements ISerializable     Private yearOfMake As Integer     Private theEngine As Engine      Private miles As Integer = 0     Public Sub New(ByVal year As Integer, ByVal anEngine As Engine)         yearOfMake = year         theEngine = anEngine     End Sub     Public Overrides Function ToString() As String          Return yearOfMake & ":" & miles & ":" & theEngine.ToString()     End Function     Public Sub New( _         ByVal info As SerializationInfo, _         ByVal context As StreamingContext)         yearOfMake = info.GetInt32("yearOfMake")         theEngine = CType(info.GetValue("theEngine", _              GetType(Engine)), Engine)          Try             miles = info.GetInt32("miles")         Catch ex As Exception             'Shhhhh, let's move on quietly.         End Try     End Sub     Public Sub GetObjectData(ByVal info As SerializationInfo, _         ByVal context As StreamingContext) _             Implements ISerializable.GetObjectData         info.AddValue("yearOfMake", yearOfMake)         info.AddValue("theEngine", theEngine)          info.AddValue("miles", miles)     End Sub End Class 

In the special deserialization constructor, you catch the exception if the miles field is missing. While this approach works, the problem with it is twofold. First, as more fields are added and more versioning happens, you might end up with several of these TRy-catch blocks. The code will get cluttered and difficult to read. Second, if you look for a number of missing fields, each one triggers an exception. This will impact performance. In a sense, you are using exceptions for the wrong purpose. As you are building an application and modifying your classes, fields may very well come and go. So you should handle this situation in the normal flow of your program instead of as an exception.

It would have been nice if the SerializationInfo class had provided a way to find out if a value exists for a given key without raising an exception. Since it doesn't, you can use an enumerator to loop through the available values and populate your object. You can then use reflection to identify the target fields, and only populate the ones that actually exist in the serialization stream. The code in Examples 2-17 and 2-18 does just that.

Example 2-17. Using reflection to serialize and deserialize (C#)

C# (ReflectionToSerialize)

 //Car.cs using System; using System.Runtime.Serialization; namespace Serialization {     [Serializable]     public class Car : ISerializable      {         private int yearOfMake;         private Engine theEngine;         private int miles = 0;         public Car(int year, Engine anEngine)         {             yearOfMake = year;             theEngine = anEngine;         }         public override string ToString()         {             return yearOfMake + ":" + miles + ":" + theEngine;         }         #region ISerializable Members         public Car(SerializationInfo info,             StreamingContext context)         {              SerializationHelper.SetData(typeof(Car), this, info);         }         public virtual void GetObjectData(SerializationInfo info,             StreamingContext context)         {              SerializationHelper.GetData(typeof(Car), this, info);         }         #endregion     } } //SerializationHelper.cs using System; using System.Runtime.Serialization; using System.Reflection; namespace Serialization {     public class SerializationHelper     {         public static void SetData(             Type theType, Object instance, SerializationInfo info)         {              SerializationInfoEnumerator enumerator =                 info.GetEnumerator();             while(enumerator.MoveNext())             {                 string fieldName = enumerator.Current.Name;                 FieldInfo theField                     = theType.GetField(fieldName,                     BindingFlags.Instance |                     BindingFlags.DeclaredOnly |                     BindingFlags.Public |                     BindingFlags.NonPublic);                 if (theField != null)                 {                     theField.SetValue(instance, enumerator.Value);                 }             }         }         public static void GetData(             Type theType, Object instance, SerializationInfo info)         {              FieldInfo[] fields = theType.GetFields(                 BindingFlags.Instance |                 BindingFlags.DeclaredOnly |                 BindingFlags.Public |                 BindingFlags.NonPublic);             for(int i = 0; i < fields.Length; i++)             {                 // Do not serialize NonSerialized fields                 if(!fields[i].IsNotSerialized)                 {                     info.AddValue(fields[i].Name,                         fields[i].GetValue(instance));                 }             }         }     } } 

Example 2-18. Using reflection to serialize and deserialize (VB.NET)

VB.NET (ReflectionToSerialize)

 'Car.vb Imports System.Runtime.Serialization Imports System.Reflection <Serializable()> _ Public Class Car     Implements ISerializable     Private yearOfMake As Integer     Private theEngine As Engine     Private miles As Integer = 0     Public Sub New(ByVal year As Integer, ByVal anEngine As Engine)         yearOfMake = year         theEngine = anEngine     End Sub     Public Overrides Function ToString() As String         Return yearOfMake & ":" & miles & ":" & theEngine.ToString()     End Function     Public Sub New( _         ByVal info As SerializationInfo, _         ByVal context As StreamingContext)          SerializationHelper.SetData(GetType(Car), Me, info)     End Sub     Public Overridable Sub GetObjectData(ByVal info As SerializationInfo, _         ByVal context As StreamingContext) _             Implements ISerializable.GetObjectData          SerializationHelper.GetData(GetType(Car), Me, info)     End Sub End Class 'SerializationHelper.vb Imports System.Runtime.Serialization Imports System.Reflection Public Class SerializationHelper     Public Shared Sub SetData( _     ByVal theType As Type, ByVal instance As Object, _     ByVal info As SerializationInfo)         Dim enumerator As SerializationInfoEnumerator = _             info.GetEnumerator()         While enumerator.MoveNext()             Dim fieldName As String = enumerator.Current.Name             Dim theField As FieldInfo = _                  theType.GetField(fieldName, _                     BindingFlags.Instance Or _                     BindingFlags.DeclaredOnly Or _                     BindingFlags.Public Or _                     BindingFlags.NonPublic)             If Not theField Is Nothing Then                 theField.SetValue(instance, enumerator.Value)             End If         End While     End Sub     Public Shared Sub GetData( _            ByVal theType As Type, _            ByVal instance As Object, _            ByVal info As SerializationInfo)         Dim fields() As FieldInfo = theType.GetFields( _             BindingFlags.Instance Or _             BindingFlags.Public Or _             BindingFlags.NonPublic)         Dim i As Integer         For i = 0 To fields.Length - 1             'Do not serialize NonSerializable Fields             If Not fields(i).IsNotSerialized Then                 info.AddValue(fields(i).Name, _                  fields(i).GetValue(instance))             End If         Next     End Sub End Class 

Let's first take a look at the GetObjectData() method that performs the serialization. It calls the SerializationHelper's Getdata() method. This method serializes all fields that are not marked with a NonSerialized attribute.

In the special deserialization constructor, you call the SerializationHelper's SetData() method, which enumerates the keys in the SerializationInfo object. For each key found, the method checks if it exists in the class, and if so, sets its value.

Notice that the GetObjectData() method is virtual/Overridable and that it only serializes its own members, not those in its base class. Classes further derived from your class can take care of serializing their own members by overriding GetObjectData() and writing a special deserialization constructor, as shown in Example 2-19.

Example 2-19. Serialization and deserialization of a derived class

C# (ReflectionToSerialize)

 //DerivedCar.cs         public DerivedCar(SerializationInfo info,             StreamingContext context)             : base(info, context)         {              SerializationHelper.SetData(                 typeof(DerivedCar), this, info);         }         public override void GetObjectData(SerializationInfo info,             StreamingContext context)         {              base.GetObjectData(info, context);             SerializationHelper.GetData(                 typeof(DerivedCar), this, info);         } 

VB.NET (ReflectionToSerialize)

 'DerivedCar.vb      Public Sub New( _         ByVal info As SerializationInfo, _         ByVal context As StreamingContext)         MyBase.New(info, context)         SerializationHelper.SetData( _             GetType(DerivedCar), Me, info)     End Sub     Public Overrides Sub GetObjectData( _          ByVal info As SerializationInfo, _         ByVal context As StreamingContext)         MyBase.GetObjectData(info, context)         SerializationHelper.GetData(GetType(DerivedCar), Me, info)     End Sub 

The serialization code that uses reflection will work for fields added and removed between versions. But it does not handle a field that gets removed in one version and then added back in a later one, with the same name but with a different intent, different semantics, or a different type. The difference in type can be handled by putting in a few more checks and balances, but the difference in semantics is a hard one. Further, this approach will fail if local security settings prohibit querying for private fields using reflection.

One option to get around these problems is to use a version number and to serialize or deserialize appropriate fields based on the version number. You can use this somewhat lengthy approach if the above options will not work.

IN A NUTSHELL

Using exceptions to determine if a member should be deserialized is expensive, and is also an inappropriate use of exceptions. It is better to rely on reflection to achieve this goal, and the code is more extensible. Or handle the versioning yourself by using the version number.

SEE ALSO

Gotcha #22, "enum lacks type-safety" and Gotcha #24, "Clone() has limitations."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net