In my previous blogs on serialization we had looked at the different ways of serializing an objects in .NET. The links to my previous blogs are
Soap Serialization
XML serialization
Binary serialization
In this blog we will have a look at how to implement custom serialization using ISerializable interface. In my previous blogs on serialization we looked at ways of controlling serialization by way of applying various attributes to the class and its member variables. Applying attributes gives us some control over serialization but doesn’t provide full fledge control. Most of your day to day serialization requirement can be met with the attributes discussed in my previous articles. But if you are the one who wants to take full control over serialization, then ISerializable interface is there for you. Lets first see what is needed to implement ISerializable interface.
The ISerializable interface has got only one method, GetObjectData. In addition to implementing the GetObjectData the implementer should also take care to implement a special constructor to classes implementing ISerializable interface. The constructor is called by the system during the de-serialization process to reconstruct the serialized object. If by chance you forgot to implement the constructor, runtime error will be thrown when one tries to deserialize a class implementing ISerializable interface without the special constructor. The “System.Runtime.Serialization.SerializationException” exception message which is thrown at runtime is pasted below.
The constructor to deserialize an object of type 'ISerializableImplementation.[CLASSNAME]' was not found. |
Note: “GetObjectData” will be executed only in the case of binary serialization. For custom serialization in XML and SOAP serialization you need to implement IXmlSerializable interface.
Now lets see and try to understand the GetObjectData and the special constructor.
GetObjectData: The ISerializable method has only one method to be implemented and the signature looks like this “GetObjectData((SerializationInfo info, StreamingContext context)”. The method takes SerializationInfo object as the first argument. SerializationInfo class can be used to store the required data to serialize and deserialize an object. Its the responsibility of the implementer to fill the SerializationInfo class with the necessary data. The implementer can decide on which member variables to store and which ones he doesn’t want to save. While doing this care should be taken to see that sufficient information is serialized for the objects to be deserialized. You can use the “AddValue” overloaded methods to add values to the SerializationInfo class. StreamingContext, the second argument in the method can be used to check the source from which the serialization request has come. Use the StreamingContext.State to check the source. Usage of StreamingContext is given at the end of the blog.
Special constructor: The signature of the special constructor is as follows “(SerializationInfo info, StreamingContext context)”. The paremeters for the constructor are also the same. So I will not spend time explaining these. The special constructor is executed when an object (whose class has implemented ISerializable interface) is deserialized. If the special constructor is not implemented the system will not throw any compile time error but it will throw a runtime exception. So make sure a class which implements ISerializable interface has an explicit implementation of the special constructor which takes SerializationInfo and StreaminContext as its constructor argument. The special constructor should be private or protected if you intent to inherit the class.
Things to note before you implement ISerializable interface
Keep the following things in mind before you implement the ISerializable Interface.
Type 'ISerializableImplementation.[CLASSNAME]' in Assembly 'ISerializableImplementation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable. |
-
NonSerializable attribute should not be used as it will not have any effect.
-
Special constructor with the same argument as that of GetObjectData method must be implemented and should be private or protected.
-
If the ISerializable implemented class is derived from a base class which also implements ISerializable interface then the parent class’ GetObjectData and special constructor should be called before the child class’ function and special constructor.
That’ enough about the ISerializable interface implementation, now you will be eager to see the real implementation. The implementation is also pretty easy and straight forward. As usual lets take the Car class and implement ISerializable interface to it. The code sample is pasted below.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.Serialization; namespace ISerializableImplementation { [Serializable] public class Car : ISerializable { public Car() {} public Car(SerializationInfo info, StreamingContext context) { this.Color = info.GetString("CarColor"); this.CubicCentimeter = info.GetDouble("CC"); this.ModelName = info.GetValue("Model", typeof(string)).ToString(); } public string Color { get; set; } public string ModelName { get; set; } public int Price { get; set; } public double CubicCentimeter { get; set; } #region ISerializable Members void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("CarColor", this.Color); info.AddValue("Model", this.ModelName); info.AddValue("CC", this.CubicCentimeter); } #endregion } } |
In the above code we have a special constructor and the GetObjectData method needed to implement custom serialization. GetObjectData method is called when a Car object is serialized and the special constructor is executed when the de-serialization of the Car object happens. Also if you notice, inside, GetObjectData method we are changing the property names, “Color” is changed to “CarColor”, “ModelName” is changed to “Model” and “CubicCentimeter” to CC. These are simple e.g. where we are taking control of serialization. Similar to this you can take control based on different scenarios. If you notice we have not included the “Price” in the serialization and deserialization process i.e. in GetObjectData and special constructor respectively.
Isn’t it so simple to implement custom serialization. Now lets see how to use the StreamingContext object in serialization and deserialization. “StreamingContext” has only two properties which can be of interest, they are “Context” and “State”. Context property can hold any type of object as the underlining type is an “System.Object” class. Context property can be used when you expect some sort of object to come from the caller when serialization is happening. State property tells us from which source the serialization request has been raised. Lets understand with an e.g.
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("CarColor", this.Color); info.AddValue("Model", this.ModelName); info.AddValue("CC", this.CubicCentimeter); //If the request comes from other sources then retrieve the price from the HashTable in the "Context" // property of StreamingContext class. if (context.State == StreamingContextStates.Other) { info.AddValue("Price", ((System.Collections.Hashtable)context.Context)["Ferari Mondeno"].ToString()); } } |
In the above code I have pasted the modified “GetObjectData” method as the other structure of the Car class is same. What we have done in the method is we have added a checking to check whether the serialization request is coming from other sources, if it is then get the price value from the “Context” property of the StreamingContext object. If you see the code I am casting the object to Hashtable and retrieving the price by passing “Ferari Mondeno” string as key. Lets see how the serialization code for the above implementation should be called.
//Create a Hashtable object and add the necessary data. System.Collections.Hashtable ht = new System.Collections.Hashtable(); ht.Add("Ferari Mondeno", 324324); ht.Add("Lamborgini", 56677); //Create StreamingContext instance by passing the Hashtable //as one of the arguments. Also we are setting the //StreaminContextStates as Other. StreamingContext sc = new StreamingContext(StreamingContextStates.Other, ht); //Pass the StreamingContext instance as one of the parameters //of the binary formatter class. BinaryFormatter bf = new BinaryFormatter(null, sc); using (System.IO.FileStream fs = new System.IO.FileStream("Serialized.txt", System.IO.FileMode.Create)) { Car car = new Car { ModelName = "Ferari Mondeno", Color = "Ferari Red", CubicCentimeter = 23584.33, Price = 234656 }; //bf.Serialize(Console.OpenStandardOutput(), car); bf.Serialize(fs, car); } |
In the above code you can see that we are first creating a Hashtable with the necessary data. Once that is done we are creating a StreamingContext instance by passing the Hashtable instance and setting the “StreamingConstextstate as “Other”. After this we are passing the StreamingContext instance as one of the arguments to the “BinaryFormatter” class’ constructor. As you would have noticed we are passing Hashtable instance into the “Context” property of the StreamingContext object. Using the Hashtable we are retrieving the price in “GetObjectData” method.
Now lets see how to deserialize object by making use of the StreamingContext instance. Based on the above changes we need to first change the constructor. Below is the constructor code.
public Car(SerializationInfo info, StreamingContext context) { this.Color = info.GetString("CarColor"); this.CubicCentimeter = info.GetDouble("CC"); this.ModelName = info.GetValue("Model", typeof(string)).ToString(); //Checking whether the serialization request is //coming from file or from other sources. if (context.State == StreamingContextStates.File || context.State == StreamingContextStates.Other) { this.Price = int.Parse(((System.Collections.Hashtable)context.Context)["Ferari Mondeno"].ToString()); } } |
In the above code we have just added a if checking to check whether the request is coming from a file or from other sources, If so read the price value from the StreamingContext instance’ Context property. The “Context” property has a Hashatable having prices.
Now lets see the deserialization code for the above.
using (System.IO.FileStream fs = new System.IO.FileStream("Serialized.txt", System.IO.FileMode.Open)) { //Create a Hashtable object and add the necessary data. System.Collections.Hashtable ht = new System.Collections.Hashtable(); ht.Add("Ferari Mondeno", 324324); ht.Add("Lamborgini", 56677); //Create StreamingContext instance by passing the Hashtable //as one of the arguments. Also we are setting the //StreaminContextStates as Other. StreamingContext sc = new StreamingContext(StreamingContextStates.File, ht); //Pass the StreamingContext instance as one of the parameters //of the binary formatter class. BinaryFormatter bf = new BinaryFormatter(null, sc); Car deserializedCar = (Car)bf.Deserialize(fs); string nl = Environment.NewLine; Console.WriteLine(nl + "Model name: {0}, " + nl + "envi Color: {1}, " + nl + "Cubic centimeter: {2}, " + nl + "Price: {3}", deserializedCar.ModelName, deserializedCar.Color, deserializedCar.CubicCentimeter, deserializedCar.Price); } |
There is nothing much to explain about the above code. Its pretty much straight forward and is similar to the code where we have serialized the Car class. That’ it about custom serialization using ISerializable interface. One very important point which I would like to reiterate again is that the “GetObjectData” and the special constructor will be executed only for binary serialization. For XML and SOAP serialization “GetObjectData” and the special constructor will not be executed. To implement custom serialization in XML and SOAP serialization you have to implement IXmlSerializable interface. We will have a look at IXmlSerializable interface implementation in my next blog. Till then try to know more.
Sandeep