4. Type-Version Tolerance
In .NET 1.1, there had to be absolute
compatibility between the metadata used to serialize a type and the
metadata used to deserialize a type. This meant that if your application
had clients with the serialized state of your types, your type members'
metadata had to be immutable, or you would break those clients.
In .NET 2.0, the formatters acquired some
version-tolerance capabilities. The tolerance is with respect to changes
in the type metadata, not changes to the assembly version itself.
Imagine a class-library vendor that provides a serializable component.
The various client applications are responsible for managing the
serialization medium (typically a file). Suppose the vendor changes the
component definition, by adding a member variable. Such a change does
not necessitate a version change, because binary compatibility is
maintained. New client applications can serialize the new component
properly. However, the serialization information captured by the old
applications is now incompatible, and will result in a SerializationException
if used in .NET 1.1. The vendor can, of course, increment the assembly
version number, but doing so will prevent the old clients from taking
advantage of the new functionality. The formatters in .NET 2.0 were
redesigned to handle such predicaments.
In the case of removing an unused member
variable, the binary formatter will simply ignore the additional
information found in the stream. For example, suppose you use this class
(but not a struct) definition and serialize it using a binary
formatter:
//Version 1.0
[Serializable]
public class MyClass
{
public int Number1;
public int Number2;
}
Without changing the assembly version, remove one of the member variables:
//Version 2.0
[Serializable]
public class MyClass
{
public int Number1;
}
You can then rebuild, redeploy, and deserialize instances of version 2.0 of MyClass with the serialization information captured using version 1.0 of MyClass.
The real challenge in type-version tolerance
is dealing with new members, because the old serialization information
does not contain any information about them. By default, the formatters
are not tolerant toward the new members and will throw an exception when
they encounter them.
.NET 2.0 addresses this problem by providing a field attribute called OptionalField—a simple attribute with a single public property of type int, called VersionAdded:
[AttributeUsage(AttributeTargets.Field,Inherited = false)]
public sealed class OptionalFieldAttribute : Attribute
{
public int VersionAdded(get;set);
}
Applying the OptionalField attribute has no effect during serialization, and fields marked with it will be serialized into the stream. This is because OptionalField
is meant to be applied on new fields of your type, and it causes the
formatters to ignore the new members during deserialization:
//Version 1.0
[Serializable]
public class MyClass
{
public int Number1;
}
//Version 2.0
[Serializable]
public class MyClass
{
public int Number1;
[OptionalField]
public int Number2;
}
That said, if the new member variable has a
good-enough default value, such as the application's default directory
or user preferences, you can use values provided by the new clients to
synthesize values for the old clients. You will need to provide these
values in your handling of the deserializing event. If you do so before
deserialization and the stream does contain serialized values, the
serialized values are preferable to the synthesized ones, and the
deserialization process will override the values you set in the handling
of the deserializing event.
Consider, for example, this class version:
//Version 1.0
[Serializable]
public class MyClass
{
public int Number1;
}
Suppose you want to add a new class member called Number2, while using the old serialization information. You need to provide handling to the deserializing event, and in it initialize Number2:
[Serializable]
public class MyClass
{
public int Number1;
[OptionalField]
public int Number2;
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
Number2 = 123;
}
}
But what if the values you synthesize are somehow
dependent on the version of the class in which they are added? You can
store version information in the OptionalField attribute, using its VersionAdded member:
[OptionalField(VersionAdded = 1)]
public int Number2;
In the deserializing event handler you will need to use reflection to read the value of the VersionAdded field and act accordingly, as shown in Example 3. This example uses the helper method OptionalFieldVersion( ) of the SerializationUtil static helper class. OptionalFieldVersion( ) accepts the type and the member variable name to reflect, returning the value of the VersionAdded field:
public static string OptionalFieldVersion(Type type,string member);
Example 3. Relying on VersionAdded
[Serializable]
public class MyClass
{
public int Number1;
[OptionalField(VersionAdded = 1)]
public int Number2;
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
int versionAdded;
versionAdded = SerializationUtil.OptionalFieldVersion(typeof(MyClass),
"Number2");
if(versionAdded == 1)
Number2 = 123;
if(versionAdded == 2)
Number2 = 456;
}
}
public static class SerializationUtil
{
public static int OptionalFieldVersion(Type type,string member)
{
Debug.Assert(type.IsSerializable);
MemberInfo[] members = type.GetMember(member,BindingFlags.Instance |
BindingFlags.NonPublic|
BindingFlags.Public |
BindingFlags.DeclaredOnly);
Debug.Assert(members.Length == 1);
object[] attributes =
members[0].GetCustomAttributes(typeof(OptionalFieldAttribute),false);
Debug.Assert(attributes.Length == 1);//Exactly one attribute is expected
OptionalFieldAttribute attribute;
attribute = attributes[0] as OptionalFieldAttribute;
return attribute.VersionAdded;
}
}
|
The next version of the serialization
infrastructure (a part of Indigo) will provide a formatter that is
version-tolerant, as well as support for version-added information for
the optional fields. |