
Pro CSharp 2008 And The .NET 3.5 Platform [eng]
.pdf
712 CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION
By doing so, the entire state of the object can be persisted out using a few lines of code. Without getting hung up on the details for the time being, consider the following Main() method:
static void Main(string[] args)
{
//Assume UserPrefs defines the following properties.
UserPrefs userData= new UserPrefs(); userData.WindowColor = "Yellow"; userData.FontSize = "50";
//The BinaryFormatter persists state data in a binary format.
BinaryFormatter binFormat = new BinaryFormatter();
//Store object in a local file.
using(Stream fStream = new FileStream("user.dat", FileMode.Create, FileAccess.Write, FileShare.None))
{
binFormat.Serialize(fStream, userData);
}
Console.ReadLine();
}
While it is quite simple to persist objects using .NET object serialization, the processes used behind the scenes are quite sophisticated. For example, when an object is persisted to a stream, all associated data (base class data, contained objects, etc.) are automatically serialized as well. Therefore, if you are attempting to persist a derived class, all data up the chain of inheritance comes along for the ride. As you will see, a set of interrelated objects is represented using an object graph.
.NET serialization services also allow you to persist an object graph in a variety of formats. The previous code example made use of the BinaryFormatter type; therefore, the state of the UserPrefs object was persisted as a compact binary format. You are also able to persist an object graph into SOAP or XML format using other types. These formats can be quite helpful when you wish to ensure that your persisted objects travel well across operating systems, languages, and architectures.
Finally, understand that an object graph can be persisted into any System.IO.Stream-derived type. In the previous example, you persisted a UserPrefs object into a local file via the FileStream type. However, if you would rather store an object to a specific region of memory, you could make use of a MemoryStream type instead. All that matters is the fact that the sequence of data correctly represents the state of objects within the graph.
The Role of Object Graphs
As mentioned, when an object is serialized, the CLR will account for all related objects to ensure the data is persisted correctly. This set of related objects is referred to as an object graph. Object graphs provide a simple way to document how a set of objects refer to each other and do not necessarily map to classic OO relationships (such as the “is-a” or “has-a” relationship), although they do model this paradigm quite well.
Each object in an object graph is assigned a unique numerical value. Keep in mind that the numbers assigned to the members in an object graph are arbitrary and have no real meaning to the outside world. Once all objects have been assigned a numerical value, the object graph can record each object’s set of dependencies.
As a simple example, assume you have created a set of classes that model some automobiles (of course). You have a base class named Car, which “has-a” Radio. Another class named JamesBondCar extends the Car base type. Figure 21-1 shows a possible object graph that models these relationships.


714CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION
(radioID) that has been marked [NonSerialized] and will therefore not be persisted into the specified data stream:
[Serializable] public class Radio
{
public bool hasTweeters; public bool hasSubWoofers; public double[] stationPresets;
[NonSerialized]
public string radioID = "XF-552RR6";
}
Next, insert two additional class types to represent the JamesBondCar and Car base classes, both of which are also marked [Serializable] and define the following pieces of field data:
[Serializable] public class Car
{
public Radio theRadio = new Radio(); public bool isHatchBack;
}
[Serializable]
public class JamesBondCar : Car
{
public bool canFly; public bool canSubmerge;
}
Be aware that the [Serializable] attribute cannot be inherited from a parent class. Therefore, if you derive a class from a type marked [Serializable], the child class must be marked [Serializable] as well, or it cannot be persisted. In fact, all objects in an object graph must be marked with the [Serializable] attribute. If you attempt to serialize a nonserializable object using the BinaryFormatter or SoapFormatter, you will receive a SerializationException at runtime.
■Note Because the XmlSerializer type does not make use of object graphs, you are not technically required to mark types with the [Serializable] attribute in order to persist an object’s state as XML. However, to ensure that your types can be persisted in all possible formats, you will typically always want to mark types that will be persisted with the serialization attributes.
Public Fields, Private Fields, and Public Properties
Notice that in each of these classes, I have defined the field data as public, just to simplify the example. Of course, private data exposed using public properties would be preferable from an OO point of view. Also, for the sake of simplicity, I have not defined any custom constructors on these types, and therefore all unassigned field data will receive the expected default values.
OO design principles aside, you may wonder how the various formatters expect a type’s field data to be defined in order to be serialized into a stream. The answer is, it depends. If you are persisting an object’s state using the BinaryFormatter or SoapFormatter it makes absolutely no difference. These types are programmed to serialize all serializable fields of a type, regardless of whether they are public fields, private fields, or private fields exposed through public properties. Do recall, however, that if you have points of data that you do not want to be persisted into the

CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION |
715 |
object graph, you can selectively mark public or private fields as [NonSerialized], as done with the string field of the Radio type.
The situation is quite different if you make use of the XmlSerializer type, however. This type will only serialize public data fields or private data exposed by public properties. Private data not exposed from properties will be ignored. For example, consider the following serializable Person type:
[Serializable] public class Person
{
// A public field.
public bool isAlive = true;
//A private field. private int personAge = 21;
//Public property/private data. private string fName = string.Empty; public string FirstName
{
get { return fName; } set { fName = value; }
}
}
When processed by the BinaryFormatter or SoapFormatter, you would indeed find that the isAlive, personAge, and fName fields are each saved into the selected stream. However, the XmlSerializer would not save the value of personAge, as this piece of private data is not encapsulated by a type property. If you wished to persist the age of the person with the XmlSerializer, you would need to define the field publicly or encapsulate the private member using a public property.
Choosing a Serialization Formatter
Once you have configured your types to participate in the .NET serialization scheme by applying the necessary attributes, your next step is to choose which format (binary, SOAP, or XML) should be used when persisting your object’s state. Each possibility is represented by the following classes:
•BinaryFormatter
•SoapFormatter
•XmlSerializer
The BinaryFormatter type serializes your object’s state to a stream using a compact binary format. This type is defined within the System.Runtime.Serialization.Formatters.Binary namespace that is part of mscorlib.dll. Therefore, if you wish to gain access to this type, simply specify the following C# using directive:
// Gain access to the BinaryFormatter in mscorlib.dll. using System.Runtime.Serialization.Formatters.Binary;
The SoapFormatter type persists an object’s state as a SOAP message. This type is defined within the System.Runtime.Serialization.Formatters.Soap namespace that is defined within a separate assembly. Thus, to format your object graph into a SOAP message, you must first set a reference to System.Runtime.Serialization.Formatters.Soap.dll using the Visual Studio 2008 Add Reference dialog box and then specify the following C# using directive:

716 CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION
// Must reference System.Runtime.Serialization.Formatters.Soap.dll. using System.Runtime.Serialization.Formatters.Soap;
Finally, if you wish to persist a tree of objects as an XML document, you have the XmlSerializer type. To use this type, you will need to specify that you are using the System.Xml. Serialization namespace and set a reference to the assembly System.Xml.dll. As luck would have it, all Visual Studio 2008 project templates automatically reference System.Xml.dll, therefore you will simply need to use the following namespace:
// Defined within System.Xml.dll. using System.Xml.Serialization;
The IFormatter and IRemotingFormatter Interfaces
Regardless of which formatter you choose to make use of, be aware that each of them derives directly from System.Object, so they do not share a common set of members from a serializationcentric base class. However, the BinaryFormatter and SoapFormatter types do support common members through the implementation of the IFormatter and IRemotingFormatter interfaces (strange as it may seem, the XmlSerializer implements neither).
System.Runtime.Serialization.IFormatter defines the core Serialize() and Deserialize() methods, which do the grunt work to move your object graphs into and out of a specific stream. Beyond these members, IFormatter defines a few properties that are used behind the scenes by the implementing type:
public interface IFormatter
{
SerializationBinder Binder { get; set; } StreamingContext Context { get; set; } ISurrogateSelector SurrogateSelector { get; set; } object Deserialize(System.IO.Stream serializationStream);
void Serialize(System.IO.Stream serializationStream, object graph);
}
The System.Runtime.Remoting.Messaging.IRemotingFormatter interface (which is leveraged internally by the .NET remoting layer) overloads the Serialize() and Deserialize() members into a manner more appropriate for distributed persistence. Note that IRemotingFormatter derives from the more general IFormatter interface:
public interface IRemotingFormatter : IFormatter
{
object Deserialize(Stream serializationStream, HeaderHandler handler); void Serialize(Stream serializationStream, object graph,
Header[] headers);
}
Although you may not need to directly interact with these interfaces for most of your serialization endeavors, recall that interface-based polymorphism allows you to hold an instance of BinaryFormatter or SoapFormatter using an IFormatter reference. Therefore, if you wish to build a method that can serialize an object graph using either of these classes, you could write the following:
static void SerializeObjectGraph(IFormatter itfFormat, Stream destStream, object graph)
{
itfFormat.Serialize(destStream, graph);
}

CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION |
717 |
Type Fidelity Among the Formatters
The most obvious difference among the three formatters is how the object graph is persisted to the stream (binary, SOAP, or XML). You should be aware of a few more subtle points of distinction, specifically how the formatters contend with type fidelity. When you make use of the BinaryFormatter type, it will persist not only the field data of the objects in the object graph, but also each type’s fully qualified name and the full name of the defining assembly (name, version, public key token, and culture). These extra points of data make the BinaryFormatter an ideal choice when you wish
to transport objects by value (e.g., as a full copy) across machine boundaries for .NET-centric applications.
The SoapFormatter persists traces of the assembly of origin through the use of an XML namespace. For example, recall the Person type earlier in this chapter. If this type were persisted as a SOAP message, you would find that the opening element of Person is qualified by the generated xmlns. Consider this partial definition, taking note of the a1 XML namespace:
<a1:Person id="ref-1" xmlns:a1= "http://schemas.microsoft.com/clr/nsassem/SimpleSerialize/MyApp%2C%20 Version%3D1.0.0.0%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull"> <isAlive>true</isAlive>
<personAge>21</personAge> <fName id="ref-3"></fName>
</a1:Person>
However, the XmlSerializer, does not attempt to preserve full type fidelity and therefore does not record the type’s fully qualified name or assembly of origin. While this may seem like a limitation at first glance, the reason has to do with the open-ended nature of XML data representation.
Here is a possible XML representation of the Person type:
<?xml version="1.0"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<isAlive>true</isAlive>
<PersonAge>21</PersonAge> <FirstName />
</Person>
If you wish to persist an object’s state in a manner that can be used by any operating system (Windows XP, Mac OS X, and various Linux distributions), application framework (.NET, J2EE, COM, etc.), or programming language, you do not want to maintain full type fidelity, as you cannot assume all possible recipients can understand .NET-specific data types. Given this, SoapFormatter and XmlSerializer are ideal choices when you wish to ensure as broad a reach as possible for the persisted tree of objects.
Serializing Objects Using the BinaryFormatter
To illustrate how easy it is to persist an instance of the JamesBondCar to a physical file, let’s first make use of the BinaryFormatter type. Again, the two key methods of the BinaryFormatter type to be aware of are Serialize() and Deserialize():
•Serialize(): Persists an object graph to a specified stream as a sequence of bytes
•Deserialize(): Converts a persisted sequence of bytes to an object graph

718 CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION
Assume you have created an instance of JamesBondCar, modified some state data, and want to persist your spy mobile into a *.dat file. The first task is to create the *.dat file itself. This can be achieved by creating an instance of the System.IO.FileStream type (see Chapter 20). At this point, simply create an instance of the BinaryFormatter and pass in the FileStream and object graph to persist. Consider the following Main() method:
//Be sure to import the System.Runtime.Serialization.Formatters.Binary
//and System.IO namespaces.
static void Main(string[] args)
{
Console.WriteLine("***** Fun with Object Serialization *****\n");
//Make a JamesBondCar and set state.
JamesBondCar jbc = new JamesBondCar(); jbc.canFly = true;
jbc.canSubmerge = false;
jbc.theRadio.stationPresets = new double[]{89.3, 105.1, 97.1}; jbc.theRadio.hasTweeters = true;
//Now save the car to a specific file in a binary format.
SaveAsBinaryFormat(jbc, "CarData.dat"); Console.ReadLine();
}
The SaveAsBinaryFormat() method is implemented as so:
static void SaveAsBinaryFormat(object objGraph, string fileName)
{
// Save object to a file named CarData.dat in binary.
BinaryFormatter binFormat = new BinaryFormatter();
using(Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
{
binFormat.Serialize(fStream, objGraph);
}
Console.WriteLine("=> Saved car in binary format!");
}
As you can see, the BinaryFormatter.Serialize() method is the member responsible for composing the object graph and moving the byte sequence to some Stream-derived type. In this case, the stream happens to be a physical file. Again, you could also serialize your object types to any Stream-derived type such as a memory location, network stream, and so forth. Once you run your program, you can view the contents of the CarData.dat file that represents this instance of the JamesBondCar by navigating to the \bin\Debug folder of the current project. Figure 21-2 shows this file opened within Visual Studio 2008.

CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION |
719 |
Figure 21-2. JamesBondCar serialized using a BinaryFormatter
Deserializing Objects Using the BinaryFormatter
Now suppose you want to read the persisted JamesBondCar from the binary file back into an object variable. Once you have programmatically opened CarData.dat (via the File.OpenRead() method), simply call the Deserialize() method of the BinaryFormatter. Be aware that Deserialize() returns a generic System.Object type, so you need to impose an explicit cast, as shown here:
static void LoadFromBinaryFile(string fileName)
{
BinaryFormatter binFormat = new BinaryFormatter();
// Read the JamesBondCar from the binary file. using(Stream fStream = File.OpenRead(fileName))
{
JamesBondCar carFromDisk = (JamesBondCar)binFormat.Deserialize(fStream);
Console.WriteLine("Can this car fly? : {0}", carFromDisk.canFly);
}
}
Notice that when you call Deserialize(), you pass the Stream-derived type that represents the location of the persisted object graph. Once you cast the object back into the correct type, you will find the state data has been retained from the point at which you saved the object.
Serializing Objects Using the SoapFormatter
Your next choice of formatter is the SoapFormatter type. The SoapFormatter will persist an object graph into a SOAP message, which makes this formatter a solid choice when you wish to distribute objects remotely across diverse environments. If you are unfamiliar with the Simple Object Access Protocol (SOAP) specification, in a nutshell, SOAP defines a standard process in which methods may be invoked in a platformand OS-neutral manner.
Assuming you have set a reference to the System.Runtime.Serialization.Formatters.Soap.dll assembly (and imported the System.Runtime.Serialization.Formatters.Soap namespace), you could persist and retrieve a JamesBondCar as a SOAP message simply by replacing each occurrence of BinaryFormatter with SoapFormatter. Consider the following new method of the Program class, which serializes an object to a local file:

720CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION
//Be sure to import System.Runtime.Serialization.Formatters.Soap
//and reference System.Runtime.Serialization.Formatters.Soap.dll. static void SaveAsSoapFormat (object objGraph, string fileName)
{
//Save object to a file named CarData.soap in SOAP format.
SoapFormatter soapFormat = new SoapFormatter();
using(Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
{
soapFormat.Serialize(fStream, objGraph);
}
Console.WriteLine("=> Saved car in SOAP format!");
}
As before, simply use Serialize() and Deserialize() to move the object graph into and out of the stream. If you call this method from Main() and run the application, you can open the resulting *.soap file. Here you can locate the XML elements that mark the stateful values of the current JamesBondCar as well as the relationship between the objects in the graph via the #ref tokens (see Figure 21-3).
Figure 21-3. JamesBondCar serialized using a SoapFormatter
Serializing Objects Using the XmlSerializer
In addition to the SOAP and binary formatters, the System.Xml.dll assembly provides a third formatter, System.Xml.Serialization.XmlSerializer, which can be used to persist the public state of a given object as pure XML, as opposed to XML data wrapped within a SOAP message. Working with this type is a bit different from working with the SoapFormatter or BinaryFormatter type. Consider the following code, which assumes you have imported the System.Xml.Serialization namespace:
static void SaveAsXmlFormat(object objGraph, string fileName)
{
// Save object to a file named CarData.xml in XML format.

CHAPTER 21 ■ INTRODUCING OBJECT SERIALIZATION |
721 |
XmlSerializer xmlFormat = new XmlSerializer(typeof(JamesBondCar), new Type[] { typeof(Radio), typeof(Car) });
using(Stream fStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
{
xmlFormat.Serialize(fStream, objGraph);
}
Console.WriteLine("=> Saved car in XML format!");
}
The key difference is that the XmlSerializer type requires you to specify type information that represents each subelement nested within the root. The first constructor argument of the XmlSerializer defines the root element of the XML file, while the second argument is an array of System.Type types that hold metadata regarding the subelements. If you were to look within the newly generated XML file (assuming you indeed call this new method from within Main()), you would find the XML data shown in Figure 21-4.
Figure 21-4. JamesBondCar serialized using an XmlSerializer
■Note The XmlSerializer demands that all serialized types in the object graph support a default constructor (so be sure to add it back if you define custom constructors). If this is not the case, you will receive an
InvalidOperationException at runtime.
Controlling the Generated XML Data
If you have a background in XML technologies, you are well aware that it is often critical to ensure the data within an XML document conforms to a set of rules that establish the validity of the data. Understand that a “valid” XML document does not have to do with the syntactic well-being of the XML elements (e.g., all opening elements must have a closing element). Rather, valid documents conform to agreed-upon formatting rules (e.g., field X must be expressed as an attribute and not a subelement), which are typically defined by an XML schema or document-type definition (DTD) file.