Introduction
Before going into detail, let's discuss.
What is Serialization & Deserialization in .NET?
Serialization is a process of converting an object into a stream of bytes. Whereas deserialization is another way around i.e converting a stream of bytes into objects.
Here are some examples where we see the need for Serialization.
- A set of objects to be sent over a network onto the other machine. Ex: WCF and remoting.
- You can save the application state in the local machine and then restore it when required.
- Serialization is mainly required for the cloning of data (Deep cloning).
Formatters
The namespace for the serialization is System.Runtime.Serialization.
- .NET supports 2 types of formats.
- Soap formatter (System.Runtime.Serialization.Formatter.Soap).
- Binary format (System.Runtime.Serialization.Formatter.Binary).
You can use XmlSerializer and DataContractSerializer for Serialization and Deserialization of XML.
Quick Start
Let’s start with an example of using a memory stream.
var namesDictionary = new Dictionary<int, string>()
{
{ 1, "Alex" },
{ 2, "Stephan" },
{ 3, "Thomas" }
};
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, namesDictionary);
stream.Position = 0;
namesDictionary = null;
var result = (Dictionary<int, string>)formatter.Deserialize(stream);
foreach (var item in result.Values)
{
Console.WriteLine(item);
}
}
The code looks easy. Isn’t it?
The memory stream is found in System.IO namespace. Memory stream represents an in-memory stream of data.
You can even serialize the data in the file. You have to use FileStream instead of MemoryStream.
FileStream represents a file in the computer. File stream is used to read from, write to, open and close files using FileMode enumeration.
var namesDictionary = new Dictionary<int, string>()
{
{ 1, "Alex" },
{ 2, "Stephan" },
{ 3, "Thomas" }
};
using (FileStream stream = new FileStream(@"C:\Sample\sample.txt", FileMode.OpenOrCreate))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, namesDictionary);
stream.Position = 0;
namesDictionary = null;
var result = (Dictionary<int, string>)formatter.Deserialize(stream);
foreach (var item in result.Values)
{
Console.WriteLine(item);
}
}
When you open the sample.txt file, you can see the assembly’s file name, version number, culture, and public key token information. While Deserializing, the formatter (in our case Binary formatter) first grabs the assembly information i.e. assembly name, version number, culture, and public key token and it ensures the assembly is loaded using Assembly.Load method.
If the assembly information doesn’t match then SerializationException will be thrown.
Note. Serialized method internally uses reflection in order to identify the object’s data type.
Usage of Serializable attributes
Let’s take another example. In this example, I created a class called Addition.
public class Addition
{
private int _value1;
private int _value2;
public int sum;
public Addition(int value1, int value2)
{
_value1 = value1;
_value2 = value2;
sum = _value1 + _value2;
}
}
In the Main method, we will use the same code as we used in the quick start example.
try {
using(MemoryStream stream = new MemoryStream()) {
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, new Addition(1, 2));
stream.Position = 0;
Addition addition = (Addition) formatter.Deserialize(stream);
Console.WriteLine(addition.sum);
}
} catch (SerializationException ex) {
Console.WriteLine(ex.ToString());
}
After running this code, a serialization exception is thrown saying the Additional class has to be marked with the Serializable attribute.
After changing code
[Serializable]
public class Addition
{
private int _value1;
private int _value2;
[NonSerialized]
public int sum;
public Addition(int value1, int value2)
{
_value1 = value1;
_value2 = value2;
sum = _value1 + _value2;
}
}
After applying the Serializable attribute, all the fields in the class are serialized. In the addition class example, I don’t want to serialize the sum field as the value will change if the value1 and value2 are changed and are easily calculated.
After running, the sum value is 0 because we marked the sum as a Non-serializable attribute. So what to do next?
For these types of issues, Microsoft has come up with 4 different attributes: OnSerializing, OnSerialized, OnDeserializing, and OnDeserialized. Execution flow will happen in the same order I mentioned before i.e OnSerializing, OnSerialized, OnDeserializing and OnDeserialized.
After applying the OnDeserialized attribute code
[Serializable]
public class Addition
{
private int _value1;
private int _value2;
[NonSerialized]
public int sum;
public Addition(int value1, int value2)
{
_value1 = value1;
_value2 = value2;
sum = _value1 + _value2;
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
sum = _value1 + _value2;
}
}
After running you can see the value as 3 in the output window.
Note. You can use the OptionalField attribute instead of the Non-Serialized attribute for the sum field. After applying the OptionalField attribute, the OnDeserialized method is no more required.
ISerializable Interface
Now the question is why ISerializable is required when we have OnSerializing, OnSerialized, OnDerializing, OnDeserialized, and OptionalField.
A virtualizable interface has many advantages.
- Total control over all the attributes.
- .ISerializable interface will help in improving the application performance. With the previous approach, internally we were using reflection.
[Serializable]
public class Employee : ISerializable
{
public int Id { get; set; }
public string Name { get; set; }
public Employee()
{
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
private Employee(SerializationInfo info, StreamingContext context)
{
Id = info.GetInt32("Id");
Name = info.GetString("Name");
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Id", Id);
info.AddValue("Name", Name);
}
}
ISerializable interface has a GetObjectData method, which takes serializationInfo and StreamingContext as a parameter.
In short, the GetObjectData method is used for serialization while the private constructor is used for deserialization.
AddValue in the GetObjectData method is used to add serialization information for the type. While Deserializing, we are using GetInt32 and GetString to get the stream of objects.
Note
- The GetObjectData method and private constructor (Deserialization) are intended to be used by the formatter and there are chances of data manipulation. So it’s always recommended to use the SecurityPermission attribute.
- While deserializing, you can even use GetValue(“name”, Type) instead of GetInt32, GetString, etc.
ISerializationSurrogate
If the class is not marked with a Serialization attribute then ISerializationSurrogate comes in handy. Serialization surrogate has some advantages.
- ISerializationSurrogate is used when the type is not originally designed to be serialized.
- It’s useful to map a version of a type to a different version of a type.
public class Employee
{
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
}
public class EmployeeSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Employee emp = (Employee) obj;
info.AddValue("Id", emp.Id);
info.AddValue("Name", emp.Name);
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Employee emp = (Employee) obj;
emp.Id = info.GetInt32("Id");
emp.Name = info.GetString("Name");
return emp;
}
}
static void Main(string[] args)
{
using(MemoryStream stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
SurrogateSelector selector = new SurrogateSelector();
selector.AddSurrogate(typeof(Employee), new StreamingContext(StreamingContextStates.All), new EmployeeSurrogate());
formatter.SurrogateSelector = selector;
formatter.Serialize(stream, new Employee
{
Id = 1, Name = "abc"
});
stream.Position = 0;
var result = (Employee) formatter.Deserialize(stream);
Console.WriteLine(result.Name);
}
Console.ReadLine();
}
Here, the GetObjectData method is used for serialization, and SetObjectData is used for Deserialization.
StreamingContext
Streaming context is the structure that describes the source or destination of the serialized stream. The state property in the StreamingContext holds a value from the StreamingContextState enumeration that indicates the destination of object data during Serialization and source of data during Deserialization.
StreamingContextState enumeration looks like the following,
[Serializable, Flags]
[System.Runtime.InteropServices.ComVisible(true)]
public enum StreamingContextStates
{
CrossProcess = 0x01,
CrossMachine = 0x02,
File = 0x04,
Persistence = 0x08,
Remoting = 0x10,
Other = 0x20,
Clone = 0x40,
CrossAppDomain = 0x80,
All = 0xFF,
}
By default, streamingContextState is set to All.
We will see how to create deep cloning. I created an extension method for deep cloning.
public static class SerilizationExtension
{
public static T DeepClone<T>(this object obj)
{
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Context = new StreamingContext(StreamingContextStates.Clone);
formatter.Serialize(stream, obj);
stream.Position = 0;
return (T)formatter.Deserialize(stream);
}
}
}
Now, we will see how to use this extension method,
[Serializable]
public class Employee : ISerializable
{
public int Id { get; set; }
public string Name { get; set; }
public Employee()
{
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
private Employee(SerializationInfo info, StreamingContext context)
{
Id = info.GetInt32("Id");
Name = info.GetString("Name");
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Id", Id);
info.AddValue("Name", Name);
}
}
class Program
{
static void Main(string[] args)
{
Employee employee = new Employee
{
Id = 1,
Name = "abc"
};
var result = employee.DeepClone<Employee>();
Console.WriteLine(result.Id);
Console.WriteLine(result.Name);
Console.ReadLine();
}
}
Hope this article helped you.