Binary Serialization

Summary

Introduction

Serialization is the process of converting the state of an object into a form that can be persisted in a storage medium or transported across processes/machines. The opposite of serialization is deserialization which is a process that converts the outcome of serialization into the original object. The .NET Framework offers two serialization technologies, binary and XML.

Binary Serialization

Binary serialization preserves type system fidelity. Type system fidelity denotes that type information is not lost in the serialization process. For example when serialization some class say MyClass, type fidelity ensures that during deserialization, an object of type MyClass will be constructed. Note that to leverage type system fidelity, both ends participating in serialization and deserialization must use the same type system. Type fidelity and hence binary serialization is useful when you want to pass objects between clients and servers. Note that XML Serialization does not provide type fidelity.

During the serialization process, public and private fields of the object and the name of the class and the containing assembly are converted to a stream of bytes written to a data stream. During deserialization, an exact replica of the object along with type information is reconstructed.

XML Serialization

This is covered in the XML Serialization chapter.

Serialization Concepts

Why would you want to use serialization. The ability to convert objects to and from a byte stream can be incredibly useful. Here are a few examples:

Persistent Storage

Imagine having an application with 100s of objects, and you are required to save the state (fields) of each object to a database. Serialization provides a convenient design pattern for achieving this objective with minimal effort. 

The CLR manages how objects are laid out in memory and provides an automated serialization mechanism using reflection. When an object is serialized, the name of the class, its containing assembly and all fields are written to storage. When an object is serialized, the CLR also keeps track of all referenced objects already serialized to ensure that the same object is not serialized more than once. The serialization architecture provided with the .NET Framework handles object graphs and circular references automatically. The only requirement for objects referenced from the serialized object is that those referenced objects be marked as [Serializable]. Else, the CLR will throw an exception when the serialize attempts to serialize an un-serializable object. 

Marshal By Value

Objects are only valid in the application domain where they were created. Attempting to pass an object as a parameter or as a return value across application domains will fail unless:

When an object derives form MarshalByRefObject, an object reference will be passed from one application domain to another rather than the object itself. When an object is marked with [Serializable], the object will be automatically serialized, transported from one application domain to another and then deserialized to produce an exact copy of the object in the second application domain.  Note then that while MarshalByRefObject passes a reference, [Serializable] causes the object to be copied.

Serialization Guidelines

When designing new classes, serialization should be one of the issues to consider. For example,

When in doubt, mark the class as serializable unless,

Basic Serialization

The class that performs the actual serialization and deserialization is called a formatter. A formatter is a class type that implements System.Runtime.Serialization.IFormatter interface to serialize/deserialize an object graph (a graph is a generalized tree, and in a serialization/deserialization context, an object graph represents a generalized tree with the serialized/deserialized object being at the root of the tree. The formatter walks the object graph to make sure that all objects are serialized/deserialized)

When a type is designed, the developer must make an explicit decision as to whether to allow instances of this type of be serialized or not. Note the following points:

Example 1

The following code provides an example on how to perform basic serialization / deserialization:

[Serializable()]
public class MyBasicClass
{
    private string strPrivate;
    public string strPublic;

    public MyBasicClass( string s1, string s2 )
    {
        strPrivate = s1;
        strPublic = s2;
    }
}

private void btnBasicBinary_Click(object sender, System.EventArgs e)
{
    SerializeMyBasicCObject();
    DeSerializeMyBasicCObject(); 
}

private void SerializeMyBasicCObject()
{
    // Create a serializable instance
    MyBasicClass ob = new MyBasicClass( "Private field data", "Public field data" );

    // Initialize a storage medium to hold the serialized object (the stream can be an object of any type
    // derived from System.IO.Stream abstract base class - i.e., you can serialize to MemoryStream,
    // FileStream, NetworkStream and so on)
    Stream stream = new FileStream( "TestFile.bin", FileMode.Create, FileAccess.Write, FileShare.Write);

    // Serialize an object into the storage medium referenced by 'stream' object.
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    formatter.Serialize( stream, ob );

    // Cleanup
    stream.Close();
}

private void DeSerializeMyBasicCObject()
{
    // Read the file back into a stream
    Stream stream = new FileStream( "TestFile.bin", FileMode.Open, FileAccess.Read, FileShare.Read);

    // Now create a binary formatter (it is up to you to ensure that code uses the same formatter for serialization
    // and deserialization
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

    // Deserialize the object and use it. Note: Constructors will not be called
    MyBasicClass ob = (MyBasicClass)formatter.Deserialize( stream );
    Trace.WriteLine( ob.strPublic );

    // Cleanup
    stream.Close();
}

Contents of TestFile.bin which holds a serialized object of type MyBasicClass (relevant parts have been highlighted):

 ÿÿÿÿ  KSerialization, Version=1.0.1614.14171, Culture=neutral, PublicKeyToken=null Serialization.MyBasicClass 
strPrivate strPublic  Private field data Public field data

Note the following:

Example 2

All objects serialized with BinaryFormatter can be deserialized with it, which makes it the ideal choice for serializing/deserializing objects on the .NET platform. If portability is a requirement, use SoapFormatter instead. You can use the same code above except that BinaryFormatter should be replaced with SoapFormatter as follows:

// Repleace this:
System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

// with this
System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();

The SoapFormatter produces the following serialized object when used in the code above;

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:MyBasicClass id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/Serialization/Serialization%2C%20Version%3D1.0.1614.15098%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<strPrivate id="ref-3">Private field data</strPrivate>
<strPublic id="ref-4">Public field data</strPublic>
</a1:MyBasicClass>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Example 3

A serialized object graph can be sent to many locations: same process, different process on the same machine, different process on a different machine, and so on. In certain cases, an object might want to know where it will be deserialized so that it initialize its state differently. For example, an object that wraps a Semaphore handle might decide to serialize its handle if it knows that deserialization will take place in the same process. The object might decide to serialize the semaphore's name if it knows that deserialization will take place in the same machine. Finally, the object might decide to throw an exception if it knows that deserialization will take place in a different machine. In other words, how does the object know where it is being deserialized?

The answer is simple: StreamingContext. A method that receives a StreamingContext object can use StreamingContext.State property to determine the source or destination of the object being serialized/deserialized. The following example shows how serialization can be used to clone an object:

private object DeepClone( object obSource )
{
    // Initialize a storage medium to hold the serialized object
    Stream stream = new MemoryStream();

    // Construct a serialization formatter
    BinaryFormatter formatter = new BinaryFormatter();

    // See Streaming Context section for an explanation of this line
    formatter.Context = new StreamingContext( StreamingContextStates.Clone );

    // Serialize the object graph into the memory stream
    formatter.Serialize( stream, obSource );

    // Reset the stream's position counter
    stream.Position = 0;

    // Clone the object by deserializing the source object into a target object
    object obTarget = formatter.Deserialize( stream );~
    return obTarget;

    // Cleanup
    stream.Close();
}

Example 4

It is possible (and can be quite useful) to serialize multiple objects into the same stream:

private Stream SerializeMultipleObjects()
{
    // Initialize a storage medium to hold the serialized object
    Stream stream = new MemoryStream();

    // Serialize an object into the storage medium referenced by 'stream' object.
   
BinaryFormatter formatter = new BinaryFormatter();

    // Serialize multiple objects into the stream
    formatter.Serialize( stream, obOrders );
    formatter.Serialize( stream, obProducts );
    formatter.Serialize( stream, obCustomers );

    // Return a stream with multiple objects
    return stream;
}

private void DeSerializeMultipleObject(Stream stream)
{
    // Construct a binary formatter
    BinaryFormatter formatter = new BinaryFormatter();

    // Deserialize the stream into object
    Orders     obOrders    = (Orders)formatter.Deserialize( stream );
    Products   obProducts  = (Products)formatter.Deserialize( stream );
    Customers  obCustomers = (Customer)formatter,Deserialize( stream )
}

Formatters

Formatters know how to serialize the complete object graph by walking the object graph and referring to each object's metadata. The Serialize method uses reflection to discover instance fields in each object's type, and if any of these fields refer to other objects, the Serialize method will know how to serialize them as well.

Formatters have very intelligent algorithms. They know how to serialize each object in the graph to the stream no more than once. For example, if two objects in the object graph are actually the same object (they both reference the same location), then the formatter detects this and serializes the object only once (and hence avoids entering into an infinite loop). As for deserialization, the formatter examines the contents of the byte stream, constructs objects of all instances that are in the stream (recall that constructors are not called when objects are deserialized), and initializes fields in all these objects so they have the same values they had when the object graph was serialized.

Fromatters and Assembly Names
Serializing

When formatters serialize an object, the full name of the type (i.e., MyNamespace.MySubNamespace.MyClass) and the name of the assembly containing the type (i.e., MyAssembly.dll) are written to the byte stream. By default, BinaryFormatter and SoapFormatter types output the assembly's full identity (name, version, public key, and culture). To make these formatter output just the simple assembly name for each serialized type, set the formatter's AssemblyFormat property to FormatterAssemblyStyle.Simple (the default is FormatterAssemblyStyle.Full.)

Deserializing

When formatters deserialize an object, they first get the assembly identity and ensure that the assembly is loaded into the executing AppDomain. How this assembly is loaded depends on the value of the formatter's AssemblyFormat:

After an assembly has been loaded, the formatter looks in the assembly for a type matching that of the object being serialized. If there is no matching type, an exception is thrown and no more objects can be serialized. If a matching type is found, an instance type is created (constructor not called) and its fields are initialized from the values present in the stream.

Formatters and dynamically loaded assemblies

Some application may use Assembly.LoadFrom to load an assembly and then construct objects from types defined in the assembly. Such object can be serialized with no problems. However, when deserializing such objects, the formatter attempts to load the assembly using Assembly.Load or Assembly.LoadWithPartialName instead of calling Assembly.LoadFrom method. In most cases, the assembly will fail to load (most likely not found) and an exception will be thrown.

The solution is to implement a method whose signature matches the System.ResolveEventHandler delegate and register this method with System.AppDomain.AssemblyResolve event just before deserializing an object (unregister this method with the event after deserializing the object). Whenever the formatter fails to load an assembly, the CLR will call your System.ResolveEventHandler method passing in the identity of the assembly that failed to load. This information can be used by the method to extract the assembly file name and construct the path where the application knows the assembly file can be found. This method then calls Assembly.LoadFrom to load the assembly and return the resulting assembly reference back from the System.ResolveEventHandler method.

Formatters and internal processing

This section provides more insight into how a formatter serializes/deserializes an object. This knowledge will help you understand how this process really works.

The following steps describe how a formatter automatically serializes an object whose type has the [Serializable] attribute applied to it:

The following steps describe how a formatter automatically deserializes an object whose type has the [Serializable] attribute applied to it:

Selective Serialization

In general it is recommended that most types be made serializable since this grants a lot of flexibility to your users. However, recall that serialization reads all of an object's fields regardless of whether the fields are declared as public, internal, protected, or private. You might want to use selective serialization if the type contains sensitive data or if the data had no meaning if transferred (i.e., windows handles, thread ids,  etc. )

Selective serialization means controlling which fields should serialize and which fields should not serialize. For example, it does not make sense to serialize a field containing the thread ID of the currently executing thread because that thread will probably not be alive when the object is deserialized. Selective serialization is achieved by annotating the class with [Serializable] attribute and then further annotating fields that should not be serialized with [NonSerialized]:

[Serializable()]
public class Circle
{
    // The following fields will be serialized
    private double dRadius;

    // The following fields will not be serialized
    [NonSerialized] private double dArea;

    public Circle( doubel dR )
    {
        dRadius = dR;
        dArea = Math.PI * dRadius * dRadius;
    }
}

// Construct a Circle object
Circle cir = new Circle(  10.0 );

When an object is constructed, the radius and the area are set to proper values. When the object gets serialized, only the value of dRadius will be written to the stream. This is exactly what you want since dArea is a calculated field and there is no need to serialize it. However, you have a problem when the object is deserialized - when deserialized, the Circle object will get its dRadius value set to 10.00, but dArea will be set to 0! The following code shows how to fix the problem:

[Serializable()]
public class Circle : IDeserializationCallback
{
    // The following fields will be serialized
    private double dRadius;

    // The following fields will not be serialized
    [NonSerialized] private double dArea;

    public Circle( doubel dR )
    {
        dRadius = dR;
        dArea = Math.PI * dRadius * dRadius;
    }

    // IDeserializationCallback implementation
    public void IDeserializationCallback.OnDeserialization( object oSender )
    {
        dArea = Math.PI * dRadius * dRadius;
    }
}

Objects are reconstructed from the inside out, and calling methods during deserialization can have unwanted effects since the methods called might refer to objects that have not be deserialized yet. During deserialization, formatters check if the type implements IDeserializationCallback interface, and if so, add this object to an internal list. After all objects have been deserialized, the formatter walks this list and calls each object's OnDeserialization. OnDeserialization therefore, should be used to do any additional work that would be necessary to fully deserialize the object. A hashtable is a typical example of a class that is difficult to deserialize without using the IDeserialiationCallback interface.

Custom Serialization

Introduction

The [Serializable] attribute is used to indicate that the given class can be serialized. The [Serializable] attribute further indicates that the class will be serialized using the default serialization process which excludes all fields marked with the [NonSerialized] attribute. ISerializable interface on the other hand allows an object to completely control its own serialization and deserialization processes. In other words, apply [Serializable] attribute to indicate that the class can be serialized and implement ISerializable interface to control the serialization process. Note that if ISerializable is implemented, then [Serializable] must also be applied, otherwise, the following error is generated:

Controlling the serialization process through ISerializable involves implementing the GetObjectData() and a special constructor:

Example 1

The code below shows how to implement ISerializable:

// A simple serializabel class that implements its serialization/deserialization processes
[Serializable]
public class MySerializableClass : System.Runtime.Serialization.ISerializable
{
    // Data fields
    private string     strPrivate = String.Empty;
    public  string     strPublic  = String.Empty;
    public  SomeClass  obMyClass  = null;                // SomeClass must be marked [Serializable]

    // Constructors
    public MySerializableClass( string s1, string s2 )
    {
        strPrivate = s1;
        strPublic  = s2;
        obMyClass = new SomeClass( 999 );
    }

    /* ISerializable */

    // A required constructor by ISerializable to deserialize the object. It is important to implement
    // this required constructor. Note that no warning will be given if this constructor is absent and
    // an exception will be thrown if you attempt to deserialize this object.
    // It is a good idea to make the constructor protected unless the class was sealed, in which case
    // the constructor should be marked as private. Making the constructor protected ensures that users
    // cannot explicitly call this constructor while still allowing derived classes to call it. The same
    // logic applies if this class was sealed except that there will be no derived classes.
    protected MySerializableClass( SerializationInfo info, StreamingContext ctx )
    {
        strPrivate = info.GetString( "s1" );
        strPublic  = info.GetString( "s2" );
        obMyClass  = (SomeClass) info.GetValue( "ob", typeof(SomeClass) );       // deserializing an object
    }

    // A required method by ISerializable to serialize the object. Simply add variables to be serialized
    // as name/value pairs to SerializationInfo object. Here you decide which member variables to serialize.
    //  Note that any name can be used to identify each serialized field
    public virtual void GetObjectData( SerializationInfo info, StreamingContext ctx )
    {
        info.AddValue( "s1", strPrivate );
        info.AddValue( "s2", strPublic );
        into.AddValue( "ob", obMyClass );       // Serializing an object
    }
}

private void btnSerializable_Click(object sender, System.EventArgs e)
{
    /* Prepare for serialization */
    MySerializableClass ob = new MySerializableClass( "hello", "world");
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream = new FileStream( "TestFile2.bin", FileMode.Create, FileAccess.Write, FileShare.Write);

    /* Serialize the object */
    formatter.Serialize( stream, ob );
    stream.Close();

    /* Prepare for deserialization */
    System.Runtime.Serialization.IFormatter formatter2 = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream2 = new FileStream( "TestFile2.bin", FileMode.Open, FileAccess.Read, FileShare.Read);

    /* Unserialize the object */
    MySerializableClass ob2 = (MySerializableClass)formatter2.Deserialize( stream2 );
}

When a formatter serializes an object graph, it looks at each object's type. If the type implements the ISerializable interface, the formatter will construct a new SerializationInfo object which contains the actual set of values that should be serialized for the object. Once the SerializationInfo object is constructed, the formatter calls the type's GetObjectData passing it the SerializationInfo object. The GetObjectData method is responsible for determining the state of the object that should be serialized, and adds this information to the SerializationInfo object. GetObjectData indicates which information should be serializes by calling one of the overloaded AddValues methods. AddValue should be called once for each piece of data that you want to serialize.

Always calls AddValue to add serialization information for your type. If a field's type (SomeClass in the example) above implements ISerializeble, do not call that object's GetObjectData method. Instead, call AddValue to add the object. The formatter will determine that the object implements ISerializable and it will call GetObjectData for you.

As for deserialization, the formatter extracts an object from the stream and allocates it memory (by calling FormatterServices.GetUninitializedObject). Initially, all of this object's fields are set to null and/or 0. Then the formatter checks if the type implements ISerializable, and if so, the formatter calls a special constructor whose signature is similar to tat GetObjectData. After the formatter finishes executing this constructor, the object should be fully deserialized. Because the deserialized type may include fields that refer to other objects, you should not execute any code in the special constructor that can access members on these objects as they are not yet fully constructed.

If the deserialized type must call members on referenced objects,  then consider implementing IDeserializationCallback. This concept was discussed here.

Example 2

You are free to derive a class that inherits from a base class that implements the ISerializable interface. If the derived class does not implement ISerializable (i.e., has no special serialization/deserialization requirements), you do not need to do anything special (you still do not need to do anything even if the derived class was marked [Serializable]). However, if the derived class implements the ISerializable interface as well, then you need to observe the following:

These requirements are illustrated below:

[Serializable]
public class MyDerivedSerializableClass : MySerializableClass
{
    public int nNum;

    // Special ISerializble constructor. Make sure you call the base constructor
   
public MyDerivedSerializableClass( string s1, string s2 ) : base( s1, s2 ) {}

    protected MyDerivedSerializableClass( SerializationInfo info, StreamingContext ctx ) : base( info, ctx)
    {
        nNum = info.GetInt32( "n1" );
    }

    // GetObjectData. Make sure you call the base class's GetObjectData
   
public override void GetObjectData( SerializationInfo info, StreamingContext ctx )
    {
        base.GetObjectData( info, ctx );
        info.AddValue( "n1", nNum );
    }
}

private void btnDerivedSerializable_Click(object sender, System.EventArgs e)
{
    /* Prepare for serialization */
    MyDerivedSerializableClass ob = new MyDerivedSerializableClass("hello", "world");
    System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream = new FileStream( "TestFile3.bin", FileMode.Create, FileAccess.Write, FileShare.Write);

    /* Serialize the object */
    formatter.Serialize( stream, ob );
    stream.Close();

    /* Prepare for deserialization */
    System.Runtime.Serialization.IFormatter formatter2 = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
    Stream stream2 = new FileStream( "TestFile3.bin", FileMode.Open, FileAccess.Read, FileShare.Read);

    /* Unserialize the object */
    MyDerivedSerializableClass ob2 = (MyDerivedSerializableClass)formatter2.Deserialize( stream2 );
}

Example 3

This example shows the general approach taken to serialize/deserialize the System.Collections.Hashtable class. Note the use of IDeserializationCallback to populate the hashtable's keys and values; IDeserializationCallback will only be called after all objects in the Hashtable class (including the actual keys and values) have been completely deserialized:

public class Hashtable : ISerializable, IDeserializationCallback
{
    // Data members
    //Internal data members not shown...
   
SerializationInfo siSaved = null;

    // Constructors
   
protected Hashtable( SerializationInfo si, StreamingContext ctxt )
    {
        // Save the serialization info. We want to postpone populating the 
        // hashtable until the key/value object have been deserialized
        siSaved = si;
    }

    // ISerialization impelentation
   
public virtual void GetObjectData( SerializationInfo si, StreamingContext ctxt )
    {
        // Call AddValue methods on SerializationInfo object to manually
        // add most fields of this Hashtable object (fields and AddValue
        // code not shown)
        // ...

        // Create object arrays to hold the Hashtable's keys and values
       
object[] keys = new object[ nHashtableEntryCount ];
        object[] values = new object[ nHashtableEntryCount ];

        // Populate the keys and values arrays with entries from the 
        // Hashtable (not shown)

        // Add the key/value objects the Hashtable
        si.AddValue( "key", keys );
        si.AddValue( "values", values );
    }

    // IDeserializationCallback implementation
   
public virtual void OnDeserialization( object oSender )
    {
        // All objects in the stream have been deserialized. We can populate
        // the Hashtable object now
       
object[] keys = (object[])siSaved.GetValue( "keys", typeof(object[]) );
        object[] values = (object[])siSaved.GetValue( "values", typeof(object[]) );

        // Add each (key,value] pair to the actual Hashtable
       
for (int x = 0; x < keys.Length; x++ )
            Add( keys[x], values[x] );

        // Set this field to null so that it be reclaimed by the garbage collector
        siSaved = null;
    }
}

ISerializable and IFormatConverter

When the SerializationInfo object is being constructed by the formatter, it (SerializationInfo) is passed an object whose type implements IFormatterConverter interface. This object knows how to convert values between the core types, such as converting an Int32 to Int64. To convert a value between other non-core types, this object casts the serialized (or original value) to an IConvertible interface and then calls the appropriate interface method.

To allows objects of a serializable type to be serialized as different types, consider having this type implement the IConvertible interface. The IFormatterConverter is only used when deserializing objects and when calling a Get method whose type does not match the type of the value in the stream.

ISerializable and Base types

ISerializable allows you to take full control over how a type gets serialized/deserialized. This power comes at a cost since the type is now responsible for serializing all its base type's fields as well. Recall the following limitations:

[Serializable]
public class Person
{ ... }

[Serializable]
public class Employee : Person
{ ... }

[Serializable]
public class Manager : Employee, ISerializable
{ ... }

Class Manager wants to take control of its own serialization by implementing ISerializable, but its base classes do not implement ISerializable. Class Manager cannot therefore, call the base's implementation of GetObjectData and the special constructor because simply they do not exist. Class Manager must perform the base class serialization manually. In the code below, FormatterServices provides static methods to aid with the serialization process  The full code is shown below:

[Serializable]
public class Person
{
    private string strTitle;
    public Person(string s) { strTitle = s; }
}

[Serializable]
public class Employee : Person
{
    private string strTitle;
    public Employee(string s) : base(s) { strTitle = s; }
}

[Serializable]
public class Manager : Employee, ISerializable
{
    private string strTitle;
    public Manager( string s ) : base(s) { strTitle = s; }

    // ISerializable implementation
    public virtual void GetObjectData( SerializationInfo si, StreamingContext ctxt )
    {
        // Serialize values from this class
        si.AddValue( "title", strTitle );

        // Get all serializable members for this class and its base classes (this is the main call)
        System.Reflection.MemberInfo[] aMI = FormatterServices.GetSerializableMembers( this.GetType(), ctxt );

        // We now know which fields should be serialized (including those from the base classes)
        for (int i = 0; i < aMI.Length; i++)
        {
            // Do not serialize fields from this base class. They have already been serialized
            // in the AddValue method above
            if (aMI[i].DeclaringType == this.GetType() ) continue;

            // We have a member from the base types. Serialize it
            si.AddValue( aMI[i].Name, ((FieldInfo)aMI[i]).GetValue( this ) );
        }
    }

    // Special Constructor
    protected Manager( SerializationInfo si, StreamingContext ctxt) : base( String.Empty )
    {
        // Note that call above to the base class constructor that takes a string. If base
        // class implemented ISerializable, we would have called the base's special 
        // constructor

        // Get all serializable members for this class and its base classes (this is the main call)
        System.Reflection.MemberInfo[] aMI = FormatterServices.GetSerializableMembers( this.GetType(), ctxt );

        // We now know which fields should be deserialized (including those from the base classes)
        for (int i = 0; i < aMI.Length; i++)
        {
            // Do not de-serialize fields from this base class. They have already been serialized
            // in the AddValue method above
            if (aMI[i].DeclaringType == this.GetType() ) continue;

            // We have a member from the base types. Serialize it
            object oValue = si.GetValue( aMI[i].Name, ((FieldInfo)aMI[i]).FieldType );    // Note the casting!
            ((FieldInfo)aMI[i]).SetValue( this, oValue );
        }

        // Finally, deserialize the values that were serialized for this object
        strTitle = si.GetString( "title" );
    }
}

/* Testing code */

// Create objects
Manager man = new Manager( "Yazan" );
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();

// Serialize
bf.Serialize( ms, man );

// Deserialize
ms.Seek( 0, SeekOrigin.Begin );
Manager man2 = (Manager)bf.Deserialize( ms );

ISerializable and Proxies

This section shows how to design a type that can serialize/deserialize itself to a different type. .NET has many examples of this - for example, in .NET Remoting when a client creates an instance of an SAO, the client gets a proxy object. While the type of the proxy is transparent to the client code (i.e., the client thinks it has an actual instance of an SAO), the type of the proxy object is different than the type of the server object. The following example illustrates how a serialized type is deserialized into another type:

[Serializable]
public sealed class MyClass : ISerializable
{
    // Data members
   
private string strName = "MyClass";

    // Constructors
    public MyClass( string s ) { strName = s; }
    // The special constructor will never be called becuase we will not be
    // deserializing instances of this class. Therefore, special constructor not provided

    // ISerializable implementation
   
public void GetObjectData(SerializationInfo si, StreamingContext ctxt)
    {
        si.AddValue( "name", strName );
        si.SetType( typeof(MyClassProxy) );
        // No other values need to be added
   
}
}

[Serializable]
public sealed class MyClassProxy ISerializable
{
    // Data members
   
private string strName = "MyClassProxy";

    // Special constructor
   
public MyClassProxy(SerializationInfo si, StreamingContext ctxt)
    {
        strName = si.GetString("name");
    }

    // ISerializable.GetObjectData implementation is not require
   
// public void GetObjectData(SerializationInfo si, StreamingContext ctxt) {}
   
public string Name { get { return strName; } }
}

/* Test code */

    // Create objects
    MyClass ob = new MyClass( "Yazan" );
    MemoryStream stream = new MemoryStream();
    BinaryFormatter bf = new BinaryFormatter();

    // Serialize
    bf.Serialize( stream, ob );

    // Deserialize
    stream.Seek( 0, SeekOrigin.Begin );
    MyClassProxy obProxy = (MyClassProxy)bf.Deserialize( stream );
    Trace.WriteLine( obProxy.Name );
 

Note that the proxy type must derive from ISerializable, otherwise, the following error is generated

.

Serialization Steps

Object serialization proceeds as follows when Serialize is called on a formatter object:

Versioning

Because serialization deals with member variables and not interfaces, pay special attention to adding / removing variables from classes that will be serialized across versions. This is especially true for classes that do not implement ISerializable as any addition / deletion / type modification of member field means that existing objects of the same type cannot be deserialized if they were serialized with a previous version.

If the state of the object may have to change between versions, then you have two choices:

Advanced Serialization

Surrogates

The discussion so far concentrated on how to modify a type's implementation so that the type can control its serialization and deserialization. The .NET Framework also allows code that is not part of a type's implementation to override how a type serializes/deserializes itself. In other words, class A (referred to as the surrogate) can control how class B gets serialized/deserialized. There are two main reasons why application code might want to override a type's serialization/deserialization behavior:

  1. Provides the ability to serialize a type that was not originally designed to be serialized.
  2. Provides a way to map one version of a type to another version of another type.

The basic mechanism is as follows: Define a surrogate type that takes over the actions required to serialize/deserialize an existing type by implementing ISerializationSurrogate. Then you register an instance of the surrogate type with the formatter, telling the formatter which existing type your surrogate type is responsible for acting upon. When the formatter detects that it is trying to serialize/deserialize an instance of the exiting type, it will call methods defined by the surrogate type. The following example illustrates:

// This class is not marked as [Serializable]. However, we can still serialize it using a serialization surrogate
public class Programmer
{
    public string strName = String.Empty;
    public DateTime dtDOB;

    public Programmer( string s, DateTime dob )
    {
        strName = s;
        dtDOB = dob;
    }
}

// This class is a serialization surrogate for class Programmer. It is serializable/deserializable (because it inherits 
// from ISerializationSurrogate). During serialization/deserialization, it will get an instance of the actual object
// (a Programmer instance) which will be used to obtain/set all required state
public class ProgrammerSurrogate : ISerializationSurrogate
{
    /* ISerializationSurrogate implementation */

    // Serializes a Programmer object
    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
    {
        // Get the real object
       
Programmer p = (Programmer)obj;

        // Add state from the real object so that ProgrammerSurrogate contains all state that would have
        // been present otherwise if Programmer type was serializable
        info.AddValue( "name", p.strName );
        info.AddValue( "dob", p.dtDOB );

        // Note: Programmer type has no private or protected fields. If it did, we would have to use serialization
        // to get the value of those fields
 
    }

    // Deserializes a Programmer object
   
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
    {
       // Get the real object. The object's fields are all null and no constructor has been called
        Programmer pOb = (Programmer)obj;

        // Initialize the real object. This real object (i.e, pOb) is what will be returned from the formatter's
        // Deserialize method
        pOb.strName = info.GetString( "name" );
        pOb.dtDOB = info.GetDateTime( "dob" );

        return pOb;  // (the return value could also be an object of a different type)
    }
}

public void TestSurrogates()
{
    // Select the serialization surrogate to delegate the serialization/deserialization process to.
    // Note that the SurrogateSelector type is used to indicate the type that will act as a substitute for the actual object
    SurrogateSelector ss = new System.Runtime.Serialization.SurrogateSelector();
    ss.AddSurrogate( typeof(Programmer),                                 // The type for which a surrogate is required
                     new StreamingContext( StreamingContextStates.All),  // Context-specific data
                     new ProgrammerSurrogate() );                        // The surrogate to call for type indicated in the 1st parameter

    // Tell the formatter to use our surrogate select
    BinaryFormatter bf = new BinaryFormatter();
    bf.SurrogateSelector = ss;

    // Create the stream and serialize
   
Programmer p = new Programmer("yazan", Convert.ToDateTime( "1/1/1990") );
    MemoryStream ms = new MemoryStream();
    bf.Serialize( ms, p );        // Calls GetObjectData for the specified SurrogateSelector object passing p as the first parameter

    // Rewind the stream and deserialize the stream back into an object
    ms.Position = 0;
    Programmer p2 = (Programmer)bf.Deserialize( ms );

    Trace.WriteLine( p2.strName, p2.dtDOB.ToShortTimeString() );
}

The following window shows the state of the serialized object p and the deserialized object p2:

Note in the preceding code that the surrogate type ProgrammerSurrogate does not have intimate knowledge of the Programmer type. And while the surrogate type (ProgrammerSurrogate) can easily access the fields of the Programmer type,  it cannot do the same with its non-public fields. With non-public fields, reflection would have to be used (subject to security)

Note that SurrogateSelector type maintain an internal hashtable. When SurrogateSelector.AddSurrogate is called, the Type and the StreamingContext make up the key and the ISerializeSurrogate object makes up the key's value. If a key with the same Type and SurrogateSelector already exits, an exception is thrown. By specifying the same type but with a different StreamingContext object,  you can register different surrogate types that know how to serialize and deserialize in different ways.

Surrogate Selector Chains

Multiple SurrogateSelector objects can be chained together. For example, one SurrogateSelector can be used to maintain a set of serialization surrogates for serializing types into proxies that can be remoted, while another SurrogateSelector can be used to convert version 1 types into version 2 types. When you have more than one SurrogateSelector objects, you must chain them into a linked list. ISurrogateSelector interface, which is implemented by SurrogateSelector types,  implements the appropriate methods for chaining.

Overriding Assembly / Type information 

Recall the following basic points:

However, there can be scenarios where it is required than an object be deserialized into a different type than the one it was serialized into. For example, you create a new version of a type and you want (for backwards compatibility) to be able to deserialize any already-serialized objects into the new version of the type.

Class System.Runtime.Serialization.SerializationBinder makes deserializing an object into a different type very easy. The general approach is this: define a type that derives from SerializationBinder and then set the formatter's Binder property to this SerializationBinder-derived object. After setting the Binder property, call the formatter's Deserialize method.

During deserialization, the formatter determines that a binder has been set. As each object is about to be deserialized, the formatter calls the binder's BindToType method, passing it the assembly name and the type that the formatter want to deserialize. At this point, BindToType decides what type should actually be constructed and returns this type. Note that if the new type uses simple serialization via the [Serializable] attribute, then the original type and the new type must have the same exact field names and types. This restriction disappears if the new type implements ISerializable interface. This approach is shown below:

public class TypeXToTypeZBinder : System.Runtime.Serialization.SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        string strAssemblyXName = "AssemblyX, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1234567890abcdef";
        string strAssemblyXType = "ClassX";

        string strAssemblyYName = "AssemblyY, Version=1.2.3.4, Culture=neutral, PublicKeyToken=abcdef1234567890";
        string strAssemblyYType = "ClassY";

        // Return different type if the assembly name passed in as a parameter is AssemblyX
       
Type obTypeToDeserialize = null;
        if ((assemblyName == strAssemblyXName) && (typeName == strAssemblyXType))
            obTypeToDeserialize = Type.GetType( strAssemblyYName );

        return obTypeToDeserialize;
    }
}

public void TestBinder()
{
    System.Runtime.Serialization.IFormatter formatter = new BinaryFormatter();
    formatter.Binder = new TypeXToTypeZBinder();

    // The code below deserializes stream s which contains a serialized version of ClassX
    // to an object of type ClassY
    ClassY ob = (ClassY)formatter.Deserialize( stream );
}

Delegates and Serialization

All delegates are compiles into serializable classes. This means that when serializing an object that contains a delegate member variable, the delegate's internal invocation list is serialized too. This can make serializing delegates very tricky as there are no guarantees that the target objects in the delegate's internal invocation list are also serializable. Consequently, serializing objects that contain delegates will work and sometimes it will through an exception. In addition, the object containing the delegate does not know/care about the state of the delegate. For example, this is the case when the delegate is used to manage event subscriptions - the exact number and identity of subscribers are often transient values that should persist during application sessions. Therefore, always mark delegate member variables as non-serializable using the [NonSerialized] attribute:

[Serializable]
public class MyClass
{
    [NonSerialized]
    public delegate void delAlaram( DateTime dt );

    // For events, add the field attribute qualifier so that the [NonSerialized] attribute is applied to the
    // underlying delegate rather than to the event itself
    [field:NonSerialized]
    public event delAlaram evtAlarm;
}

Binary Formatter and Serialization Events

.NET Framework 2.0 introduces support for serialization events. Designated methods on your class will be called when serialization and deserialization takes place. .NET Framework 2.0 defines four serialization and deserialization events - Serializing, Serialized, Deserializing, Deserialized. You designate methods as serialization/deserialization event handlers using method attributes as shown below:

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

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

    [OnDeserializing]
    void OnDeserializing( StreamingContext context)
    { ... }

    [OnDeserialized]
    void OnDeserialized( StreamingContext context)
    { ... }
}