Article

This article is in continuation to this post : Serialization in .NET Part 3

Custom Serialization

ISerializable interface

Sometimes simple annotation doesn't meet our requirements. Implementing the ISerializable interface for a class allows you to customize its object serialization. Rather than simply annotating the serialization attributes to a class/field, this involves writing the actual code to manage serialization. It introduces a great amount of flexibility in the way your serialization stream is organized. .NET hands over the control to an object itself when it discovers its ISerializable implementation.

You must still mark your class as Serializable, even if it implements the ISerializable interface. Otherwise, it will be discarded by the formatter before it attempts to look for the ISerializable implementation.

The ISerializable interface resides in the System.Runtime.Serialization namespace. It defines only one method, GetObjectData(). This method is automatically called when an object is serialized. It accepts an object of type SerializationInfo and another object of type StreamingContext object as parameters.

When you call Serialize() on a formatter, the formatter instantiates the SerializationInfo object and calls the GetObjectData() method by passing the SerializationInfo object reference to GetObjectData(). In the GetObjectData(), you typically populate the SerializationInfo object with the data you want to serialize.

Deserialization constructor

When you work with the ISerializable interface, you also need to implement a special constructor method for your class which is referred to as Deserialization constructor. It has the same parameters as GetObjectData() i.e. it accepts a reference to the SerializationInfo and the StreamingContext objects.

The constructor method is invoked when an object is deserialized. If you try to deserialize the object without providing an implementation for this constructor, the Deserialize() will throw a runtime error.

The job of this constructor is to retrieve the serialized data members from the SerializationInfo object. You should declare the deserialization constructor as private or protected. Normally, you don't like this constructor to be called by other users of the class. On the other hand, the deserialization process will be able to call it. It calls the constructor through the reflection API.

DataSet, Font, and Hashtable are a few of the .NET classes that implement this interface.

SerializationInfo class

It can simply be viewed as a data bag. It stores a set of name-value pairs. It does not dictate the serialization format. The serialization format is established by the formatter.

Its AddValue() method accepts the name and value as parameters and adds them in the SerializationInfo bag. It is an overloaded method that allows you to add any of the types. It is typically called by the GetObjectData() method.

When the GetObjectData() method returns the control back to the formatter, the formatter serializes the data stored in the bag. The formatter enumerates the set of name-value pairs stored in the SerializationInfo object. It then serializes each value into an element and names the element as specified in name.

When the data (the serialized name-value pairs) is deserialized afterwards, the formatter constructs a new SerializationInfo object and fills it with the name-value pairs. These pairs are the ones that the GetObjectData() stored in the original SerializationInfo object. The formatter then passes the SerializationInfo object to the deserialization constructor.

The Getxxx() methods are typically called by the deserialization constructor. They accept the name as parameter and retrieve the corresponding value from the SerializationInfo object. For e.g. GetBoolean() retrieves a Boolean value, GetInt64() retrieves a 64-bit signed integer value, and GetValue() retrieves a value of any type.

The SerializationInfo class provides the following properties: a. AssemblyName: The assembly name of the type being serialized. b. FullTypeName: The full name of the type being serialized. The full name includes the class name and namespaces. c. MemberCount: The count of members present in this instance.

Besides the data, AssemblyName, FullTypeName, MemberCount is also written to the serialization stream. This information is used during deserialization, for instance, to find out the correct type and its assembly.

StreamingContext class

The GetObjectData() receives a StreamingContext object. It holds information that describes the destination of a serialized object. The destination means the environment where the deserialization is intended to occur.

The deserialization constructor also receives a StreamingContext object. This time it holds the information that describes the source of the serialized data.

C# provides an enumeration named StreamingContextStates. The source or destination information is described using its various values, such as a. a same process b. a different process on the same machine c. can be a different machine d. persistent storage such as a database, file e. can be a different application domain

You can tune and optimize the serialized data based on this information.

For instance, if the intended destination of a serialized object is a different process running on the same machine, you can store the references, such as file handle, in the serialized stream. The receiving program doesn't need to reopen the file thus saving the overheads associated with opening the file once again.

On the other hand, if an object is going to be deserialized on a different machine, the system-wide references won't be valid in this context. In this case, a path in the following format can be stored: \\ and then the program running on the different machine will be able to access the file.

Now your ChatRoom class implements the ISerializable interface. Therefore, you need to implement these: GetObjectData() and deserialization constructor.

using System;
using System.Collections;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
class ChatRoom : ISerializable {
    private int roomId;
    private string roomName;

    //This time, you're going to serialize the onlineUsers too.
    //Note that it is no longer marked as NonSerialized
    private ArrayList onlineUsers = new ArrayList();

    public ChatRoom() { }

    public ChatRoom(int roomId, string roomName) {
        this.roomId = roomId;
        this.roomName = roomName;
    }

    //Implement the GetObjectData() and store the name-value pairs
    //in SerializationInfo object.
    public void GetObjectData(SerializationInfo info, StreamingContext context) {
        info.AddValue("RoomId", roomId);
        info.AddValue("RoomName", roomName);
        info.AddValue("OnlineUsers", onlineUsers);
    }

    //Deserialization constructor: Declare it as private and 
    //retrieve the serialized members from SerializationInfo object.
    private ChatRoom(SerializationInfo info, StreamingContext context) {
        roomId = info.GetInt32("RoomId");
        roomName = info.GetString("RoomName");

        //GetValue() returns a value of type object. Cast it to ArrayList
        onlineUsers = (ArrayList) info.GetValue(“OnlineUsers”, onlineUsers);
    }
    public void AddUser(string name) {
        onlineUsers.Add(name);
    }
}

class CustomSerializeDemo {
    public static void Main() {
        ChatRoom room1 = new ChatRoom(1, "Current Affairs");
        room1.AddUser("John");

        FileInfo fileRef = new FileInfo("ChatBinary.dat");
        Stream writer = fileRef.Create();
        BinaryFormatter binFormat = new BinaryFormatter();

        //When you serialize the object, the GetObjectData() is automatically called
        binFormat.Serialize(writer, room1);
        writer.Close();

        Stream reader = fileRef.Open(FileMode.Open, FileAccess.Read, FileShare.None);

        //When you deserialize the object, the deserialization constructor is 
        //automatically invoked 
        ChatRoom dRoom = (ChatRoom) binFormat.Deserialize(reader);
        reader.Close();
    }
}