
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf



C H A P T E R 1 7 ■ U N D E R S TA N D I N G O B J E C T S E R I A L I Z AT I O N |
547 |
When reading object graphs, you can use the phrase “depends on” or “refers to” when connecting the arrows. Thus, in Figure 17-1 you can see that the Car class refers to the Radio class (given the “has-a” relationship). JamesBondCar refers to Car (given the “is-a” relationship) as well as Radio (as it inherits this protected member variable).
Of course, the CLR does not paint pictures in memory to represent a graph of related objects. Rather, the relationship documented in the previous diagram is represented by a more mathematical formula that looks something like this:
[Car 3, ref 2], [Radio 2], [JamesBondCar 1, ref 3, ref 2]
If you parse this formula, you can again see that object 3 (the Car) has a dependency on object 2 (the Radio). Object 2, the Radio, is a lone wolf and requires nobody. Finally, object 1 (the JamesBondCar) has a dependency on object 3 as well as object 2. In any case, when you serialize or deserialize an instance of JamesBondCar, the object graph ensures that the Radio and Car types also participate in the process.
The beautiful thing about the serialization process is that the graph representing the relationships among your objects is established automatically behind the scenes. As you will see later in this chapter, however, if you do wish to become more involved in the construction of a given object graph, it is possible to do so.
Configuring Objects for Serialization
To make an object available to .NET serialization services, all you need to do is decorate each related class with the [Serializable] attribute. That’s it (really). If you determine that a given class has some member data that should not (or perhaps cannot) participate in the serialization scheme, you can mark such fields with the [NonSerialized] attribute. This can be helpful if you have member variables in a serializable class that do not need to be “remembered” (e.g., fixed values, random values, transient data, etc.) and you wish to reduce the size of the persisted graph.
To get the ball rolling, here is the Radio class, which has been marked [Serializable], excluding a single member variable (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";
}
The JamesBondCar class and Car base class are also marked [Serializable] and define the following pieces of field data:
[Serializable] public class Car
{
public Radio theRadio = new Radio(); public bool isHatchBack;
}

548 C H A P T E R 1 7 ■ U N D E R S TA N D I N G O B J E C T S E R I A L I Z AT I O N
[Serializable]
public class JamesBondCar : Car
{
public bool canFly; public bool canSubmerge;
}
Be aware that the [Serializable] attribute cannot be inherited. 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.
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 using the BinaryFormatter, it makes absolutely no difference. This type is programmed to serialize all serializable fields of a type, regardless of whether they are public fields, private fields, or private fields exposed through type properties. The situation is quite different if you make use of the XmlSerializer or SoapFormatter type, however. These types will only serialize public pieces of field data or private data exposed through public properties.
Do recall, however, that if you have points of data that you do not want to be persisted into the object graph, you can selectively mark public or private fields as [NonSerialized], as done with the string field of the Radio type.
Choosing a Serialization Formatter
Once you have configured your types to participate in the .NET serialization scheme, your next step is to choose which format should be used when persisting your object graph. As of .NET 2.0, you have three choices out of the box:
•BinaryFormatter
•SoapFormatter
•XmlSerializer
The BinaryFormatter type serializes your object graph 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, to serialize your objects using a binary format, all you need to do is specify the following C# using directive:
// Gain access to the BinaryFormatter in mscorlib.dll. using System.Runtime.Serialization.Formatters.Binary;
The SoapFormatter type represents your graph 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 set a reference to System.Runtime. Serialization.Formatters.Soap.dll and specify the following C# using directive:

C H A P T E R 1 7 ■ U N D E R S TA N D I N G O B J E C T S E R I A L I Z AT I O N |
549 |
// Must reference System.Runtime.Serialization.Formatters.Soap.dll! using System.Runtime.Serialization.Formatters.Soap;
Finally, if you wish to persist an object graph as an XML document, you will need to specify that you are using the System.Xml.Serialization namespace, which is also defined in a separate assembly: System.Xml.dll. As luck would have it, all Visual Studio 2005 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 IRemotingFormatting Interfaces
Regardless of which formatter you choose to make use of, be aware that each of them derives directly from System.Object, and therefore they do not share a common set of members from a serialization-centric base class. However, the BinaryFormatter and SoapFormatter types do support
common members through the implementation of the IFormatter and IRemotingFormatter interfaces (of which 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);
}

550 C H A P T E R 1 7 ■ U N D E R S TA N D I N G O B J E C T S E R I A L I Z AT I O N
Type Fidelity Among the Formatters
The most obvious difference among the three formatters is how the object graph is persisted to stream (binary, SOAP, or pure 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 not only persist 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. 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 (see Chapter 18). As noted, to achieve this level of type fidelity, the BinaryFormatter will account for all field data of a type (public or private).
The SoapFormatter and XmlSerializer, on the other hand, do not attempt to preserve full type fidelity and therefore do not record the type’s fully qualified name or assembly of origin, and only persist public field data/public properties. While this may seem like a limitation at first glance, the reason has to do with the open-ended nature of XML data representation. If you wish to persist object graphs that can be used by any operating system (Windows XP, Mac OS X, and *nix 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 object graph.
Serializing Objects Using the BinaryFormatter
To illustrate how easy it is to persist an instance of the JamesBondCar to a physical file, let’s 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
Assume you have created an instance of JamesBondCar, modified some state data, and want to persist your spymobile 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 16). At this point, simply create an instance of the BinaryFormatter and pass in the FileStream and object graph to persist:
using System.Runtime.Serialization.Formatters.Binary; using System.IO;
...
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;
//Save object to a file named CarData.dat in binary.
BinaryFormatter binFormat = new BinaryFormatter(); Stream fStream = new FileStream("CarData.dat",
FileMode.Create, FileAccess.Write, FileShare.None);


552 C H A P T E R 1 7 ■ U N D E R S TA N D I N G O B J E C T S E R I A L I Z AT I O N
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 using the HTTP protocol. If you are unfamiliar with the SOAP specification, don’t sweat the details right now. In a nutshell, SOAP defines a standard process in which methods may be invoked in a platformand OS-neutral manner (we’ll examine SOAP in a bit more detail in the final chapter of this book during a discussion of XML web services).
Assuming you have set a reference to the System.Runtime.Serialization.Formatters.Soap.dll assembly, you could persist and retrieve a JamesBondCar as a SOAP message simply by replacing each occurrence of BinaryFormatter with SoapFormatter. Consider the following code, which serializes an object to a local file named CarData.soap:
using System.Runtime.Serialization.Formatters.Soap;
...
static void Main(string[] args)
{
...
// Save object to a file named CarData.soap in SOAP format.
SoapFormatter soapFormat = new SoapFormatter(); fStream = new FileStream("CarData.soap", FileMode.Create, FileAccess.Write, FileShare.None);
soapFormat.Serialize(fStream, jbc); fStream.Close();
Console.ReadLine();
}
As before, simply use Serialize() and Deserialize() to move the object graph in and out of the stream. If you open the resulting *.soap file, 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. Consider the following end result (XML namespaces snipped for brevity):
<SOAP-ENV:Envelope xmlns:xsi="..."> <SOAP-ENV:Body>
<a1:JamesBondCar id="ref-1" xmlns:a1="..."> <canFly>true</canFly> <canSubmerge>false</canSubmerge> <theRadio href="#ref-3"/> <isHatchBack>false</isHatchBack>
</a1:JamesBondCar>
<a1:Radio id="ref-3" xmlns:a1="..."> <hasTweeters>true</hasTweeters> <hasSubWoofers>false</hasSubWoofers> <stationPresets href="#ref-4"/>
</a1:Radio>
<SOAP-ENC:Array id="ref-4" SOAP-ENC:arrayType="xsd:double[3]"> <item>89.3</item>
<item>105.1</item>
<item>97.1</item> </SOAP-ENC:Array>
</SOAP-ENV:Body> </SOAP-ENV:Envelope>
