Abstract
Serialization in C# .NET plays a key role in various functions, such as remoting. Developers may often need to perform custom serialization in order to have complete control over the serialization and deserialization processes. The standard .NET serialization processes will therefore not be enough to provide the developer with control over these processes. In this series of articles, Anupam Banerji explains serialization, the need for custom serialization, and how to implement custom serialization in your code.
Introduction
Serialization of objects is a new feature in the .NET Framework. Prior to serialization, the only way to store class objects was to write the objects into a stream object. This has two consequences. First, code has to be written to store each object. If the property is a user-defined type, or an object containing several other objects, then this task becomes very complicated very quickly. Second, if changes are made to class objects, then the code to store them must be changed too. This results in a doubling of effort for each change.
Serialization was introduced to provide the developer with a simple, efficient and consistent way to store class objects. There are very few requirements to implementing standard serialization. The standard .NET serialization model also includes serialization events in order to recalculate values when stored objects are retrieved.
Standard Serialization
Standard Serialization is implemented in classes through a series of attributes. To implement serialization of a class, add the [Serializable]
attribute above the class declaration. To exclude any calculated field, tag it with the [NonSerialized]
attribute.
To recalculate objects when the object is deserialized, the developer must implement the IDeserializationCallback
interface.
To serialize a class, the developer has a choice between a BinaryFormatter
object and a SoapFormatter
object. The BinaryFormatter
serialization object should be used when serialization and deserialization occurs between two .NET assemblies. The SoapFormatter
object should be used when serialization and deserialization occurs between a .NET assembly and Simple Object Access Protocol (SOAP) compliant executable. SOAP formatting will be discussed in another article.
Custom Serialization
Custom serialization is implemented through the ISerializable
interface. The interface implements the GetObjectData()
method and an overloaded constructor used for deserialization. The GetObjectData()
method is implemented as follows:
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
}
The method takes two arguments, one is the SerializationInfo
object which implements the IFormatterConverter
interface. We will use it in an example below. The StreamingContext
object contains information about the purpose of the serialized object. For example, a StreamingContext
of the Remoting type is set when the serialized object graph is sent to a remote or unknown location.
The overloaded constructor has two arguments; the SerializationInfo
object and the StreamingContext
object.
The BinaryFormatter
serialization object fires four events that the developer may implement: OnSerializing
, OnSerialized
, OnDeserializing
and OnDeserialized
. The events are implemented as attributes in the class implementing the ISerializable
interface. The methods marked as serialization events must have a StreamingContext
argument, or else a runtime exception occurs. The methods must also be marked as void
.
If both interfaces are implemented, the OnDeserialization()
method in the IDe-serializationCallback
interface is called after the OnDeserialized
event, as the example output below shows.
A Quick Example: A Custom Serialization Class
We implement both serialization interfaces in our class declaration below:
using System.Runtime.Serialization;
[Serializable]
class TestClass : ISerializable, IDeserializationCallback
{
public string Name
{
get;
private set;
}
public int ToSquare
{
get;
private set;
}
[NonSerialized]
public int Squared;
public TestClass(string name, int toSquare)
{
Name = name;
ToSquare = toSquare;
ComputeSquare();
}
public TestClass(SerializationInfo info, StreamingContext context)
{
Name = info.GetString("Name");
ToSquare = info.GetInt32("ToSquare");
Console.WriteLine("Deserializing constructor");
ComputeSquare();
}
private void ComputeSquare()
{
Squared = ToSquare * ToSquare;
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
Console.WriteLine("OnSerializing fired.");
}
[OnSerialized]
private void OnSerialized(StreamingContext context)
{
Console.WriteLine("OnSerialized fired.");
}
[OnDeserializing]
private void OnDeserializing(StreamingContext context)
{
Console.WriteLine("OnDeserializing fired.");
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
Console.WriteLine("OnDeserialized fired.");
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("Serializing...");
info.AddValue("Name", Name);
info.AddValue("ToSquare", ToSquare);
}
void IDeserializationCallback.OnDeserialization(object sender)
{
Console.WriteLine("IDeserializationCallback.OnDeserialization method.");
ComputeSquare();
}
}
The Squared field is not serialized, but recalculated when the TestClass
object is deserialized. The four events that are fired during (de)serialization illustrate the potential use of the events to the developer. The methods in this example write the progress of the (de)serialization process to the console.
We write the calling function to (de)serialize as shown:
TestClass tc = new TestClass("Test", 3);
FileStream fs = new FileStream("Serialized.txt", FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fs, tc);
fs.Close();
tc = null;
fs = new FileStream("Serialized.txt", FileMode.Open);
tc = (TestClass)bf.Deserialize(fs);
Console.WriteLine("Squared = " + tc.Squared);
The serialized output is written to the Serialized.txt file. This is a binary file containing the state of the TestClass
instance. The console output after the assembly is executed is:
OnSerializing fired.
Serializing...
OnSerialized fired.
OnDeserializing fired.
Deserializing constructor
OnDeserialized fired.
IDeserializationCallback.OnDeserialization method called.
The OnDeserialization()
method is called last, not between the OnDeserializing
and OnDeserialized
events.
We can implement a SOAP formatter instead of a binary formatter by replacing the instance of BinaryFormatter
with an instance of SoapFormatter
. To instance the SoapFormatter
, make sure that the System.Runtime.Serialization.Formatters.Soap
namespace is referenced. The .NET Framework 3.5 documentation states that the SoapFormatter
class is obsolete. However, developers will come across implementations of this class, especially in assemblies requiring portable object graphs.
TestClass tc = new TestClass("Test", 3);
FileStream fs = new FileStream("Serialized.txt", FileMode.Create);
SoapFormatter sf = new SoapFormatter();
sf.Serialize(fs, tc);
fs.Close();
tc = null;
fs = new FileStream("Serialized.txt", FileMode.Open);
tc = (TestClass)sf.Deserialize(fs);
Console.WriteLine("Squared = " + tc.Squared);
The SoapFormatter
object also supports firing the four serialization events, and the console output is identical to the output when the BinaryFormatter
object is implemented.
The SoapFormatter
creates a serialized object in a human readable format. The Serialization.txt file contains the following XML output:
<SOAP-ENV:Envelope …>
<SOAP-ENV:Body>
<a1:TestClass id="ref-1" …>
<Name id="ref-3">Test</Name>
<ToSquare>3</ToSquare>
</a1:TestClass>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The items in the serialized object are marked in blue. An XML reader in another language or platform can easily read and rebuild the TestClass
instance. The XML output can be controlled. I will address this in a later article.
Conclusion
Custom serialization in .NET allows the developer complete control of the (de)serialization process. The sequence of events and the implemented interfaces and attributes should be understood before designing a serializable class. The serialization formatter choice will dictate class design. A design that incorporates serialization processes for a wide range of applications should be implemented if there are undecided issues of deployment and integration.
This is the first article in this series.
To download this technical article in PDF format, visit the Coactum Solutions website at http://www.coactumsolutions.com.
History
- 15th July, 2010: Initial post