Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

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

.pdf
Скачиваний:
111
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

C H A P T E R 1 7

■ ■ ■

Understanding Object Serialization

In Chapter 16, you learned about the functionality provided by the System.IO namespace. As shown, this namespace provides numerous reader/writer types that can be used to persist data to a given location (in a given format). This chapter examines the related topic of object serialization. Using object serialization, you are able to persist and retrieve the state of an object to (or from) any System.IO.Stream- derived type.

As you might imagine, the ability to serialize types is critical when attempting to copy an object to a remote machine (the subject of the next chapter). Understand, however, that serialization is quite useful in its own right and will likely play a role in many of your .NET applications (distributed or not). Over the course of this chapter, you will be exposed to numerous aspects of the .NET serialization scheme, including a set of new attributes introduced with .NET 2.0 that allow you to customize the process.

Understanding Object Serialization

The term serialization describes the process of persisting (and possibly transferring) the state of an object to a stream. The persisted data sequence contains all necessary information needed to reconstruct (or deserialize) the state of the object for use later. Using this technology, it is trivial to save vast amounts of data (in various formats) with minimal fuss and bother. In fact, in many cases, saving application data using serialization services is much less cumbersome than making direct use of the readers/writers found within the System.IO namespace.

For example, assume you have created a GUI-based desktop application and wish to provide a way for end users to save their preferences. To do so, you might define a class named UserPrefs that encapsulates 20 or so pieces of field data. If you were to make use of a System.IO.BinaryWriter type, you would need to manually save each field of the UserPrefs object. Likewise, when you wish to load the data from file back into memory, you would need to make use of a System.IO.BinaryReader and (once again) manually read in each value to reconfigure a new UserPrefs object.

While this is certainly doable, you would save yourself a good amount of time simply by marking the UserPrefs class with the [Serializable] attribute. In this case, the entire state of the object can be persisted out using a few lines of code:

static void Main(string[] args)

{

// Assume UserPrefs has been marked [Serializable].

UserPrefs userData= new UserPrefs(); userData.WindowColor = "Yellow"; userData.FontSize = "50"; userData.IsPowerUser = false;

545

546C 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

//Now save object to a file named user.dat.

BinaryFormatter binFormat = new BinaryFormatter(); Stream fStream = new FileStream("user.dat",

FileMode.Create, FileAccess.Write, FileShare.None);

binFormat.Serialize(fStream, userData); fStream.Close();

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 classes, 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 a Simple Object Access Protocol (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 persist an object to 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. The set of related objects is collectively 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 17-1 shows a possible object graph that models these relationships.

Figure 17-1. A simple object graph

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);

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

551

binFormat.Serialize(fStream, jbc); fStream.Close(); Console.ReadLine();

}

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. However, you could also serialize your object types to any Stream-derived type such as a memory location, given that MemoryStream is a descendent of the

Stream type.

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 Main(string[] args)

{

...

// Read the JamesBondCar from the binary file. fStream = File.OpenRead("CarData.dat"); JamesBondCar carFromDisk =

(JamesBondCar)binFormat.Deserialize(fStream); Console.WriteLine("Can this car fly? : {0}", carFromDisk.canFly); fStream.Close();

Console.ReadLine();

}

Notice that when you call Deserialize(), you pass the Stream-derived type that represents the location of the persisted object graph (again, a file stream in this case). Now if that is not painfully simple, I’m not sure what is. In a nutshell, mark each class you wish to persist to a stream with the [Serializable] attribute. After this point, use the BinaryFormatter type to move your object graph to and from a binary stream. At this point, you can view the binary image that represents this instance of the JamesBondCar (see Figure 17-2).

Figure 17-2. JamesBondCar serialized using a BinaryFormatter

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>

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

553

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 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:

using System.Xml.Serialization;

...

static void Main(string[] args)

{

...

// Save object to a file named CarData.xml in XML format.

XmlSerializer xmlFormat = new XmlSerializer(typeof(JamesBondCar), new Type[] { typeof(Radio), typeof(Car) });

fStream = new FileStream("CarData.xml", FileMode.Create, FileAccess.Write, FileShare.None);

xmlFormat.Serialize(fStream, jbc); fStream.Close();

...

}

The key difference is that the XmlSerializer type requires you to specify type information that represents the items in the object graph. Notice that 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 CarData.xml file, you would find the following (abbreviated) XML data:

<?xml version="1.0" encoding="utf-8"?> <JamesBondCar xmlns:xsi="...">

<theRadio>

<hasTweeters>true</hasTweeters>

<hasSubWoofers>false</hasSubWoofers>

<stationPresets>

<double>89.3</double>

<double>105.1</double>

<double>97.1</double>

</stationPresets>

</theRadio>

<isHatchBack>false</isHatchBack>

<canFly>true</canFly>

<canSubmerge>false</canSubmerge>

</JamesBondCar>

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 elements within an XML document conform 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