Foreword
It's been several weeks since I last posted on my blog, due to various other commitments over the summer. Now that I have a bit more time again, I thought it was a good time to get going again.
When deciding what topic to kick-start with, I decided upon another 101 mini-series this time looking at serialization using the .NET framework. For those of you new to .NET, hopefully this should serve as a good primer for getting into serialization; and for the more experienced developers amongst you, perhaps this will serve as a useful refresher (as indeed it did for me, whilst I was preparing the sample code!).
Introduction
Serialization is the process of persisting the state of an object to some form of permanent storage medium. This is achieved by converting public
and private
fields of a objects, as well as its class name and assembly name, to a stream of bytes which is then persisted onto the selected storage medium. De-serialization, as the name suggests, reverses the process. As an aside, serialization and de-serialization are sometimes referred to as 'dehydrating' and 'rehydrating' an object respectively.
There are three main mechanisms for serialization in .NET:
- Binary serialization: Persistence of objects to a binary storage format. We will look at this in this article
- SOAP serialization: Persistence of objects in SOAP (Simple Object Access Protocol) format
- XML serialization: Persistence of objects as XML
The Serialization Farmyard
Continuing our farmyard theme from previous articles, the Serialization Farmyard is a simple Windows application in which the user can create a new farmyard, populate it with animals and then save it to disk for later use. The user can also reload a previous farmyard from disk and edit it as they see fit. The application uses binary serialization/de-serialization for saving and loading farmyards, and the object model is shown below (click to zoom):
Making Objects Serializable
Now let's take a quick look at the Animal
class:
[Serializable]
public abstract class Animal
{
public virtual string Name { get; set; }
public virtual int Arrived { get; set; }
public abstract int SpaceRequired { get; }
}
<Serializable()>
Public MustInherit Class Animal
Public Property Name As String
Public Property Arrived As Integer
Public MustOverride ReadOnly Property SpaceRequired As Integer
End Class
Notice how the class has been decorated with a Serializable
attribute? This is how we mark a class as being binary serializable (and SOAP serializable too. XML serialization uses a totally different mechanism). It is important to note however, that for an object to be fully serializable, not only must it be decorated with Serializable
attribute, but the types of all of its members must too be either serializable or explicitly marked as non-serialized (we will look at an example of this later). If not, then an exception will be thrown at run-time. In the above example, all the members are of primitive types which are always serializable.
Now you could quite easily assume that if a base class is marked as serializable, any sub-classes would therefore also be serializable. This however is not the case, as the Serializable
attribute is not inheritable. Therefore any sub-classes of our abstract Animal
class must therefore also be explicitly decorated with the Serializable
, for example:
[Serializable]
public class Sheep : Animal
{
public override int SpaceRequired
{
get { return 3; }
}
}
<Serializable()>
Public Class Sheep
Inherits Animal
Public Overrides ReadOnly Property SpaceRequired As Integer
Get
Return 3
End Get
End Property
End Class
Also, don't think you can get away with just decorating your sub-classes with the Serializable
attribute! All classes up the inheritance hierarchy must also be suitably decorated, otherwise an exception will be thrown at run-time.
Now let's look at the Farmyard
class. Firstly, the class definition:
[Serializable]
public class Farmyard
{
private List<Animal> animals;
public string Name { get; set; }
public int Day { get; set; }
public int Capacity { get; set; }
}
<Serializable()>
Public Class Farmyard
Private animals As List(Of Animal)
Public Property Name As String
Public Property Day As Integer
Public Property Capacity As Integer
End Class
As before, the class is decorated with the Serializable
attribute. The properties Name
, Day
and Capacity
are all primitive types and therefore can be serialized. If you look at the MSDN documentation, you will see that the List<T>
class is serializable, providing that the type of T
is also serializable. In this case, it will be, as our Animal
class has been decorated with the Serializable
attribute.
Now, take a look at the weather
property:
[NonSerialized]
private Weather weather;
public Weather Weather
{
get { return weather; }
set { weather = value; }
}
<NonSerialized()>
Private _weather As Weather
Public Property Weather As Weather
Get
Return _weather
End Get
Set(value As Weather)
_weather = value
End Set
End Property
This shows how to prevent a member from being serialized at run-time: simply decorate it with the NonSerialized
attribute. A common reason for doing this is when a member is of a type that is not serializable, however in our example we don't persist the Weather
property because we will be assigning it a new value, when it is loaded from disk (see below).
It's also worth noting that the NonSerialized
attribute can only be applied to fields and not properties. Therefore, we cannot use the "auto-properties" feature of C#/VB, and have to return to the pattern of declaring a private
member variable and exposing it through a separate public
property.
Now for a real gotcha: As we know, when serializing an object, the .NET serializer will attempt to serialize all members of that object which are not explicitly marked as being non-serializable. This also includes any delegates or events, or more specifically, any handlers that are currently wired up to them. In an application such as this, the objects wired up to any events are often UI objects which are generally not serializable in the first place; and even if they were, we wouldn't want to persist them to disk. Therefore, the delegates which handle any events must also be marked as non-serialized:
[NonSerialized]
private FarmyardEventHandler animalAdded;
public event FarmyardEventHandler AnimalAdded
{
add { animalAdded = (FarmyardEventHandler)Delegate.Combine(animalAdded, value); }
remove { animalAdded = (FarmyardEventHandler)Delegate.Remove(animalAdded, value); }
}
<NonSerialized()>
Private _animalAdded As FarmyardEventHandler
Public Custom Event AnimalAdded As FarmyardEventHandler
AddHandler(value As FarmyardEventHandler)
_animalAdded = DirectCast([Delegate].Combine(_animalAdded, value), FarmyardEventHandler)
End AddHandler
RemoveHandler(value As FarmyardEventHandler)
_animalAdded = DirectCast([Delegate].Remove(_animalAdded, value), FarmyardEventHandler)
End RemoveHandler
RaiseEvent(sender As Object, eventArgs As System.EventArgs)
_animalAdded(sender, eventArgs)
End RaiseEvent
End Event
In similarity to non-serialized properties, we have to declare a private
delegate, decorated with the NonSerialized
attribute, and expose it through a public
event.
Controlling and Customizing Serialization
There are two main approaches to controlling and customizing binary serialization in the .NET framework. One is to implement ISerializable
interface (which we won't look at in this article, but you can find more information in the MSDN documentation); but the Microsoft-recommended approach is to use the following attributes to decorate certain methods in your class which will be executed at certain points during the serialization/de-serialization process:
OnSerializing
OnSerialized
OnDeserializing
OnDeserialized
We can see an example of this in the Farmyard
class of our application:
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
NewDay();
}
public void NewDay()
{
Day++;
Random random = new Random();
Weather = (Weather)random.Next(0, 4);
OnNewDay(new FarmyardEventArgs());
}
<OnDeserialized()>
Private Sub OnDeserialized(ByVal context as StreamingContext)
NewDay()
End Sub
Public Sub NewDay()
Day = Day + 1
Dim random As Random = New Random()
Weather = DirectCast(random.Next(0, 4), Weather)
OnNewDay(New FarmyardEventArgs())
End Sub
As you can see, the method is decorated with the OnDeserialized
attribute which means it will be executed once de-serialization is complete. This method calls the NewDay()
method, which increments the day and sets the Weather
property to a random value. Remember, we are explicitly not serializing the weather
when we save the farmyard to disk.
Note also, that the method takes an object of type StreamingContext
, and although we don't use it in our example, it provides information about the source and destination of the current serialization stream, as well as any caller-defined data. As always, you can read more about this in the MSDN documentation.
Performing Serialization
OK, so we've looked at how we declare which classes and members are to be serialized (or not, as the case may be!), but how do we actually go about serializing our data to storage and retrieving it later? Well, the answer is we need to use a BinaryFormatter
object. This object has two methods: Serialize()
and Deserialize()
for serialization and de-serialization respectively. Here is the code for saving a farmyard to disk:
using (Stream fileStream = new FileStream(file, FileMode.Create,
FileAccess.Write, FileShare.None))
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(fileStream, farmyard);
}
Using fileStream As Stream = New FileStream(file, FileMode.Create, _
FileAccess.Write, FileShare.None)
Dim formatter As IFormatter = New BinaryFormatter
formatter.Serialize(fileStream, farmyard)
End Using
And for loading a saved farmyard from disk:
using (Stream fileStream = new FileStream(file, FileMode.Open,
FileAccess.Read, FileShare.Read))
{
IFormatter formatter = new BinaryFormatter();
farmyard = (Farmyard)formatter.Deserialize(fileStream);
}
Using FileStream As Stream = New FileStream(file, FileMode.Open, _
FileAccess.Read, FileShare.Read)
Dim formatter As IFormatter = New BinaryFormatter
farmyard = DirectCast(formatter.Deserialize(FileStream), Farmyard)
End Using
Note how the Serialize()
and Deserialize()
methods of the BinaryFormatter
object accept an object of type Stream
. In our example, we are using a FileStream
object as we are serializing our farmyard to disk, but we could just as easily have used a MemoryStream
or NetworkStream
for serializing to memory or over a network respectively.
Note also that, when de-serializing an object, a cast is required as the Deserialize()
method simply returns a method of type Object
.
Summary
Binary serialization provides an easy mechanism for persisting objects to permanent storage. By the use of various attributes, we can control and refine the serialization process to suit our particular needs.
The source code for the farmyard application can be downloaded here. Unfortunately, due to time constraints, the source code is in C# only.
Next: SOAP Serialization