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.
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 steps—typically, 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 .
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.
2.2. Using the deserialized event
Taking advantage of the deserialized event makes the use of IDeserializationCallback
redundant, as the two are logically equivalent—both let your class
respond to the post-deserialization event and initialize or reclaim
non-serializable members, while using already deserialized values.
Example 2. 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( );
}
}
|
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 do—the 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).
The formatters by default comply with .NET's general version-binding and resolving policy.
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 id="ref-1"
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.