Initializing and manipulating objects in a program is fairly straightforward, but in some situations, this isn't enough. Rather than just being used and discarded, it's sometimes necessary for an object to be preserved for later use. It's also sometimes necessary for objects to be sent to another computer. This process of preserving objects is known as serialization.
When an object is serialized, its state is stored in such a way that the object can be easily remade at a later time or on a different machine. .NET provides several methods of serializing (and deserializing) objects, as well as several formats in which the object can be saved. This article will explore the serialization functionality provided by .NET.
Simple Serialization
To get started, let's serialize a simple object: a string containing a short message. We'll store the string on disk and then restore it by reading the file. Here's the required code, which we'll examine in detail:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
class StringSerialization
{
public static void Main(string[] args)
{
// The string we want to serialize
string message = "This is a message.";
Console.WriteLine(message);
// Serialization takes place here
FileStream myStream = File.Create("message");
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(myStream, message);
// Clear our string
message = null;
// Restore the contents of our string -- deserialization
The above code saves the message in a file named message. It then clears the contents of the message (just for fun) and then restores the saved object.
As you can see, .NET's mechanisms for serialization are located in the System.Runtime.Serialization namespace. Here, we make use of the namespace System.Runtime.Serialization.Formatters.Binary. Formatters contains, as its name implies, different formatters for serialization. The formatter is in charge of exactly how the object to be serialized is represented. Here, we use BinaryFormatter. You can see how BinaryFormatter formats our string by examining the contents of the message file, which look something like this:
����This is a message.
Since we're only serializing a string, the file remains somewhat readable, with the exception of a number of special characters. However, with more complicated objects, the output produced by BinaryFormatter is quite unreadable. In many situations, it doesn't matter what the serialized object looks like; you may not care. However, in other situations, the serialized object may need to be more human-readable. This is a downside to the BinaryFormatter, but, thankfully, this isn't the only way to represent serialized data in .NET.
After message is initialized, a FileStream is created, which provides read/write access:
FileStream myStream = File.Create("message");
Note, however, that any type of Stream will do. Next, we create a BinaryFormatter, whose function was already explained:
BinaryFormatter formatter = new BinaryFormatter();
The object is then serialized simply by calling the Serialize method of our BinaryFormatter object:
formatter.Serialize(myStream, message);
Next, our position in myStream is reset to the beginning of the file so that we can begin reading. The process of deserialization is no less simple than the process of serialization. The Deserialize method is called, and myStream is passed:
Deserialize returns the object contained within the Stream, which must be cast to the proper type -- here, it is string. Finally, we clean up by closing myStream.
In the previous example, we serialized a string object. However, you are, of course, not limited to serializing types provided by the framework. The real purpose of serialization is to serialize your own types. Thankfully, the process of serializing custom types doesn't have to be difficult or complex. .NET itself does most of the work for you. All you have to do is tell it what can be serialized and what can't. This is done through attributes.
Let's take a look at a type called Person:
public class Person
{
private string name;
private int age;
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public string Name
{
get
{
return name;
}
}
public int Age
{
get
{
return age;
}
}
}
We can't simply serialize an instance of Person, or any other custom type. This is because not all types are fit for serialization. It simply wouldn't make sense for some types to be serialized. Instead, we must tell .NET that instances of our type can be serialized. This is done through the Serializable attribute:
[Serializable]
public class Person
{
...
}
Now we're free to serialize instances of our type. The process for doing this is the same as it was for serializing a string object earlier. We create a Stream object (a FileStream in our example) and a Formatter object (we'll use a BinaryFormatter again), and then we simply make a call to the Serialize method, passing the Stream and the object to be serialized. To deserialize, we call the Deserialize method. Here's everything in action:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
class PersonSerialization
{
public static void Main(string[] args)
{
// Create a Person
Person bob = new Person("Bob", 35);
Console.WriteLine("{0}, age {1}.", bob.Name, bob.Age);
// Serialize our Person
FileStream myStream = File.Create("bob");
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(myStream, bob);
// Deserialize our Person
myStream.Position = 0;
bob = (Person)formatter.Deserialize(myStream);
myStream.Close();
Console.WriteLine("{0}, age {1}.", bob.Name, bob.Age);
}
}
Bob, age 35.
Bob, age 35.
Note that if we were to try to serialize a Person object without marking Person as Serializable, we would get a SerializationException:
Unhandled Exception: System.Runtime.Serialization.SerializationException:Type Person is not marked as Serializable.
The Serializable attribute, however, marks the entire class as being serializable. In our Person example, all of the internal data in bob is serialized. While this is fine in some cases, it may be undesirable in others. It may not make sense to serialize some internal data, and some internal data may contain sensitive material. For example, consider this class:
using System;
[Serializable]
public class User
{
private string name;
private DateTime sessionStartTime;
public User(string name)
{
this.name = name;
sessionStartTime = DateTime.Now;
}
public string Name
{
get
{
return name;
}
}
public DateTime SessionStartTime
{
get
{
return sessionStartTime;
}
}
}
The class represents a computer user and contains a private field of type DateTime which represents the time that the user logged in. When an instance of User is initialized, sessionStartTime is set to the current time. However, it doesn't make sense to store this field because when the user logs off (which we'll represent by saving the class), the time is no longer valid. In order to prevent .NET from serializing a piece of data, it must be marked with the NonSerialized attribute:
[Serializable]
public class User
{
...
[NonSerialized]
private DateTime sessionStartTime;
...
}
Now, when a User object is serialized, sessionStartTime won't be included. Let's try this out:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
class UserSerialization
{
public static void Main(string[] args)
{
// Create a User
User charles = new User("Charles");
Console.WriteLine("{0}, logged on since {1}.", charles.Name, charles.SessionStartTime);
// Serialize the User
FileStream myStream = File.Create("charles");
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(myStream, charles);
// Deserialize the User
myStream.Position = 0;
charles = (User)formatter.Deserialize(myStream);
myStream.Close();
Console.WriteLine("{0}, logged on since {1}.", charles.Name, charles.SessionStartTime);
}
}
If we run the above code, we can see that sessionStartTime isn't serialized, just as we specified. However, something happens as a side effect, as can be seen from the output:
Charles, logged on since 8/2/2007 8:42:03 PM.
Charles, logged on since 1/1/0001 12:00:00 AM.
Since sessionStartTime is not serialized, .NET is forced to find a new value for it upon deserialization. It initializes it to the default value, which is evident above. Of course, this is not what we want. Instead, we want it to contain the date and time when the object was deserialized. To remedy this, .NET offers several attributes that can be applied to methods, such as OnDeserializing. Methods marked with OnDeserializing will be called during the deserialization process. We can add such a method to Person that will give sessionStartTime the proper value. Here's how it's done:
As you can see, the method has no return type, and it accepts one parameter, a StreamingContext structure. This structure contains information about the serialization process, but here, we can safely ignore it. Any method taking advantage of OnDeserializing must have no return type and must accept a StreamingContext, just like our method.
Besides the OnDeserializing attribute, .NET contains other similar attributes. When one of these attributes is applied to a method, then the method is called in the appropriate stage of serialization or deserialization. Here's a simple class that makes use of this family of attributes:
Notice how all the methods look like the OnDeserializing method in our previous example in their return type and parameter. The attribute names should make it pretty easy to tell when each method is called. Nonetheless, let's see the serialization and deserialization of an instance of our type. However, instead of using a FileStream and wasting a file, let's use a MemoryStream:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
class AttributeDemo
{
public static void Main(string[] args)
{
AttributeTest test = new AttributeTest();
// Serialize it
MemoryStream myStream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(myStream, test);
// Deserialize it
myStream.Position = 0;
test = (AttributeTest)formatter.Deserialize(myStream);