Introduction
Before going in detail, let's discuss.
What is Serialization & Deserialization in .NET
Serialization is a process of converting an object into stream of bytes. Whereas deserialization is other way around i.e converting stream of bytes into objects.
Here are some examples where we see the need of Serialization:
- A set of objects to be sent over a network on to the other machine. Ex: WCF and remoting.
- You can save application state in the local machine and then restore when required.
- Serialization is mainly required for cloning of data (Deep cloning).
Formatters
The namespace for the serialization is System.Runtime.Serialization.
- .NET supports 2 types of formatters.
- Soap formatter (System.Runtime.Serialization.Formatter.Soap).
- Binary formatter (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 memory stream.
- varnamesDictionary = newDictionary < int, string > ()
- {
- {
- 1,"Alex"
- },
- {
- 2,"Stephan"
- },
- {
- 3,"Thomas"
- }
- };
- using(MemoryStream stream = newMemoryStream())
- {
- BinaryFormatter formatter = newBinaryFormatter();
- 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?
Memory stream is found in System.IO namespace. Memory stream represents 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 = newDictionary < int,
- string > ()
- {
- {
- 1,"Alex"
- },
- {
- 2,"Stephan"
- },
- {
- 3,"Thomas"
- }
- };
- using(FileStream stream = newFileStream(@ "C:\Sample\sample.txt", FileMode.OpenOrCreate))
- {
- BinaryFormatter formatter = newBinaryFormatter();
- 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 sample.txt file, you can see 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: Serialize 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.
- publicclassAddition
- {
- privateint _value1;
- privateint _value2;
-
- publicint 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 = newMemoryStream())
- {
- BinaryFormatter formatter = newBinaryFormatter();
- formatter.Serialize(stream, newAddition(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, serialization exception is thrown saying Additional class has to be marked with Serializable attribute.
After changing code
- [Serializable]
- public class Addition
- {
- privateint _value1;
- privateint _value2;
-
- [NonSerialized]
- public int sum;
-
- public Addition(int value1, int value2)
- {
- _value1 = value1;
- _value2 = value2;
- sum = _value1 + _value2;
- }
- }
After applying serializable attribute, all the fields in the class are serialized. In the addition class example, I don’t want to serialize sum field as the value will change if the value1 and value2 are changed and is easily calculated.
After running, the sum value is 0 because we marked sum as Non-serializable attribute. So what to do next?
For these type 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 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 OptionalField attribute instead of Non-Serialized attribute for the sum field. After applying OptionalField attribute, 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?
ISerializable 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 GetObjectData method, which takes serializationInfo and StreamingContext as a parameter.
In short, GetObjectData method is used for serialization while 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: - 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 SecurityPermission attribute.
- While deserializing, you can even use GetValue(“name”,Type) instead of GetInt32, GetString, etc.
ISerializationSurrogate
If the class is not marked with Serialization attribute then ISerializationSurrogate comes handy. Serialization surrogate has some advantages:
- ISerializationSurrogate is used when type is not originally designed to be serialized.
- It’s useful to map version of type to a different version of a type.
- public classEmployee
- {
- 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 = newMemoryStream())
- {
- BinaryFormatter formatter = newBinaryFormatter();
- SurrogateSelector selector = newSurrogateSelector();
- selector.AddSurrogate(typeof(Employee), newStreamingContext(
- StreamingContextStates.All), newEmployeeSurrogate());
- formatter.SurrogateSelector = selector;
- formatter.Serialize(stream, newEmployee
- {
- Id = 1, Name = "abc"
- });
- stream.Position = 0;
- var result = (Employee) formatter.Deserialize(stream);
- Console.WriteLine(result.Name);
- }
- Console.ReadLine();
- }
Here, GetObjectData method is used for serialization and SetObjectData is used for Deserialization.
StreamingContext Streaming context is the struct which describes source or destination of serialized stream. The state property in the StreamingContext hold value from the StreamingContextState enumeration that indicates 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)]
- publicenumStreamingContextStates
- {
- 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 staticT DeepClone < T > (thisobject obj)
- {
- using(MemoryStream stream = newMemoryStream())
- {
- BinaryFormatter formatter = newBinaryFormatter();
- formatter.Context = newStreamingContext(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 = newEmployee
- {
- Id = 1, Name = "abc"
- };
- var result = employee.DeepClone < Employee > ();
-
- Console.WriteLine(result.Id);
- Console.WriteLine(result.Name);
- Console.ReadLine();
- }
- }
Hope this article helped you.