Section 9.3. Serialization Events


9.3. Serialization Events

.NET 2.0 introduces support for serialization events. .NET calls designated methods on your class when serialization and deserialization take place. Four serialization and deserialization events are defined. The serializing event is raised just before serialization takes place, and the serialized event is raised just after serialization. Similarly, the deserializing event is raised just before deserialization, and the deserialized event is raised after deserialization. Both classes and structures can take advantage of serialization events. You designate methods as serialization event handlers using method attributes, as shown in Example 9-9.

Example 9-9. Applying the serialization event attributes
 [Serializable] public class MyClass {    [OnSerializing]    void OnSerializing(StreamingContext context)    {...}    [OnSerialized]    void OnSerialized(StreamingContext context)    {...}    [OnDeserializing]    void OnDeserializing(StreamingContext context)    {...}    [OnDeserialized]    void OnDeserialized(StreamingContext context)    {...} }

Note that the class itself must still be marked for serialization. Each serialization event-handling method must have the following signature:

     void <Method Name>(StreamingContext context);

This is required because internally .NET still uses delegates to subscribe and invoke the event-handling methods. If the attributes are applied on methods with incompatible signatures, .NET will throw a SerializationException.

StreamingContext is a structure informing the object why it is being serialized. The StreamingContext structure provides the State property of the enum type StreamingContextStates. Possible reasons for serialization include remoting (across app domain or process), persisting to a file, and so on. The context parameter is largely ignored; it is used only in advanced esoteric scenarios in which the serialization and deserialization actions are context-sensitive.

The event attributes are defined in the System.Runtime.Serialization namespace.

As the attribute names imply, the OnSerializing attribute designates a method handling the serializing event, and the OnSerialized attribute designates a method handling the serialized event. Similarly, the OnDeserializing attribute designates a method handling the deserializing event, and the OnDeserialized attribute designates a method handling the deserialized event. Figure 9-2 is a UML activity diagram depicting the order in which events are raised during serialization.

Figure 9-2. Events during serialization


.NET first raises the serializing event, thus invoking the corresponding event handlers (there can be more than one, as you will see shortly). Next, .NET serializes the object, and finally the serialized event is raised and its event handlers are invoked.

Figure 9-3 is a UML activity diagram depicting the order in which deserialization events are raised.

Figure 9-3. Events during deserialization


Unlike with the serialization events, with deserialization .NET has to accommodate the use of IDeserializationCallback. .NET first raises the deserializing event and then performs the deserialization. If the class implements IDeserializationCallback, .NET then calls the OnDeserialization( ) method and finally raises the deserialized event. Note that in order to call the deserializing event-handling methods .NET has to first construct an objecthowever, it does so without ever calling your class's default constructor.

The next version of the serialization infrastructure (a part of Indigo) will provide a compatible programming model that supports an analogous set of serialization events.


9.3.1. Applying the Event Attributes

.NET allows you to apply the same serialization event attributes on multiple methods of the class. For example:

     [OnSerializing]     void OnSerializing1(StreamingContext context)     {...}     [OnSerializing]     void OnSerializing2(StreamingContext context)     {...}

The canonical example for applying the same serialization event attribute on multiple methods is when dealing with partial classes:

     [Serializable]     public partial class MyClass     {             [OnSerializing]        void OnSerializing1(StreamingContext context)        {...}     }     public partial class MyClass     {             [OnSerializing]        void OnSerializing2(StreamingContext context)        {...}     }

This decoupled the various parts of the partial class, because it enables each part of the class to deal with the class members it is responsible for, and have a dedicated serialization event handling method for them.

While you can also apply multiple attributes on the same event-handling method, as follows:

     [OnSerializing]     [OnSerialized]     void OnSerialization(StreamingContext context)     {...}

the usefulness of doing so is questionable. The method will be called once per attribute, and there is no easy way to detect which event is raised inside the method.

9.3.2. Serialization Events and Class Hierarchies

A significant advantage of using attributes for event-handler identification (as opposed to interfaces) is that the event mechanism is decoupled from the class hierarchy. When using attributes, the event-handling methods are called for each level in a class hierarchy. There is no need to call your base class's event-handling methods, and there is no problem if those base methods are private. The events are raised according to the order of the hierarchy, and the event attributes are not inherited. For example, when serializing an object of type MySubClass, defined as:

     [Serializable]     public class MyBaseClass     {        [OnSerializing]        void OnSerializing1(StreamingContext context)        {...}     }     [Serializable]     public class MySubClass : MyBaseClass     {        [OnSerializing]        void OnSerializing2(StreamingContext context)        {...}     }

the OnSerializing1( ) method is called first, followed by a call to OnSerializing2( ). The situation could therefore get messy when virtual methods are involved and the subclass overrides its base class's handling of a serialization event, and even calls it. To deal with this problem, the serialization infrastructure throws an exception of type SerializationException if any of the event attributes are applied on a virtual or abstract method or on an overriding method.

The serialization event attributes throw a SerializationException when applied on a class method that implements an interface method and are ignored when applied to a method in the interface definition.


Use of the new inheritance qualifier is still allowed in conjunction with serialization events. Since no other party besides .NET should call a serialization event-handling method, I recommend always designating such methods as private.

9.3.2.1 Using the deserializing event

Since no constructor calls are ever made during deserialization, the deserializing event-handling method is logically your deserialization constructor. It is intended for performing some custom pre-deserialization stepstypically, initialization of non-serializable members. Any value settings done on the serializable members will be in vain, because the formatter will set those members again during deserialization, using values from the serialization stream. The main difference between IDeserializationCallback and the deserializing event is that IDeserializationCallback's OnDeserialization( ) method is called after deserialization is complete, while the deserializing event is called before deserialization starts. You should only place in the deserializing event-handling method any initialization steps that are independent of the values saved in the serialization stream. In contrast, in OnDeserialization( ) you can take advantage of already deserialized members (such as a database connection string, as in Example 9-1). Other steps you can take in the deserializing event-handling method are setting specific environment variables (such as thread local storage), performing diagnostics, or signaling some global synchronization events.

9.3.2.2 Using the deserialized event

Taking advantage of the deserialized event makes the use of IDeserializationCallback redundant, as the two are logically equivalentboth let your class respond to the post-deserialization event and initialize or reclaim non-serializable members, while using already deserialized values. Example 9-10 demonstrates this point. It performs exactly the same task as Example 9-1, except it relies on the deserialized event rather than IDeserializationCallback.

Example 9-10. Initializing non-serializable resources using the deserialized event
 [Serializable] public class MyClass {    [NonSerialized]    IDbConnection m_Connection;    string m_ConnectionString;    [OnDeserialized]    void OnDeserialized(StreamingContext context)    {       m_Connection = new SqlConnection(  );       m_Connection.ConnectionString = m_ConnectionString;       m_Connection.Open(  );    } }

9.3.3. Serialization and Versioning

An application may wish to serialize the state of multiple objects of multiple types to the same stream. Consequently, a simple dump of object state will not dothe formatter must also capture each object's type information. During deserialization, the formatter needs to read the type's metadata and initialize a new object according to the information serialized, populating the corresponding fields. The easiest way to capture the type information is to record the type's name and reference in its assembly. For each object serialized, the formatter persists the state of the object (the values of the various fields) and the version and full name of its assembly, including a token of the assembly's public key (if a strong name is used). This can be seen in Example 9-5. When the formatter deserializes the object, it loads its assembly and reflects the type's metadata.

The formatters by default comply with .NET's general version-binding and resolving policy, described in Chapter 5. If the serialized type's assembly does not have a strong name, the formatters try to load a private assembly and completely ignore any version incompatibility between the version captured during serialization and the version of the assembly found. If the serialized type's assembly has a strong name, .NET insists on using a compatible assembly. If such an assembly is not found, .NET raises an exception of type FileLoadException.

Both the binary and SOAP formatters also provide a way to record only the friendly name of the assembly, without any version or public-key token, even if the assembly has a strong name. The formatters provide a public property called AssemblyFormat, of the enum type FormatterAssemblyStyle, defined in the System.Runtime.Serialization.Formatters namespace:

     public enum FormatterAssemblyStyle     {        Full,        Simple     }     public sealed class BinaryFormatter : IFormatter,...     {        public FormatterAssemblyStyle AssemblyFormat{get; set;}        //Other members, including implementation of IFormatter     }     public sealed class SoapFormatter : IFormatter,...     {        public FormatterAssemblyStyle AssemblyFormat{get; set;}        //Other members, including implementation of IFormatter     }

Note that the AssemblyFormat property is not part of the IFormatter interface. The default value of AssemblyFormat is FormatterAssemblyStyle.Full. If you set it to FormatterAssemblyStyle.Simple, no version-compatibility checks will take place during deserialization. For example, consider this SOAP serialization code:

     MyClass obj = new MyClass(  );     obj.Number1 = 123;     obj.Number2 = 456;     SoapFormatter formatter = new SoapFormatter(  );     formatter.AssemblyFormat = FormatterAssemblyStyle.Simple;     Stream stream;     stream = new FileStream(@"C:\temp\obj.xml",FileMode.Create,FileAccess.Write);     using(stream)     {        formatter.Serialize(stream,obj);     }

This code results in the following SOAP envelope body:

     <SOAP-ENV:Body>        <a1:MyClass            xmlns:a1="http://schemas.microsoft.com/clr/nsassem/           MyNamespace/MyClassLibrary">           <Number1>123</Number1>           <Number2>456</Number2>        </a1:MyClass>     </SOAP-ENV:Body>

Although this option exists, I strongly discourage you from circumventing version serialization and type verification. At best, a potential incompatibility will result in an exception of type SerializationException. At worst, your application may later crash unexpectedly because the incompatible type required some custom initialization steps.

9.3.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 OptionalFielda 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 9-11. 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 9-11. 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.




Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

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