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

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

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

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

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 an expressed as an attribute and not a subelement), which are typically defined by an XML schema or document-type definition (DTD) file.

By default, all field data of a [Serializable] type is formatted as elements rather than XML attributes. If you wish to control how the XmlSerializer generates the resulting XML document, you may decorate your [Serializable] types with any number of additional attributes from the System.Xml.Serialization namespace. Table 17-1 documents some (but not all) of the attributes that influence how XML data is encoded to a stream.

Table 17-1. Serialization-centric Attributes of the System.Xml.Serialization Namespace

Attribute

Meaning in Life

XmlAttributeAttribute

The member will be serialized as an XML attribute.

XmlElementAttribute

The field or property will be serialized as an XML element.

XmlEnumAttribute

The element name of an enumeration member.

XmlRootAttribute

This attribute controls how the root element will be constructed

 

(namespace and element name).

XmlTextAttribute

The property or field should be serialized as XML text.

XmlTypeAttribute

The name and namespace of the XML type.

 

 

By way of a simple example, first consider how the field data of JamesBondCar is currently persisted as XML:

<?xml version="1.0" encoding="utf-8"?>

<JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">

...

<canFly>true</canFly>

<canSubmerge>false</canSubmerge>

</JamesBondCar>

If you wished to specify a custom XML namespace that qualifies the JamesBondCar as well as encodes the canFly and canSubmerge values as XML attributes, you can do so by modifying the C# definition of JamesBondCar as so:

[Serializable,

XmlRoot(Namespace = "http://www.intertechtraining.com")] public class JamesBondCar : Car

{

...

[XmlAttribute] public bool canFly;

[XmlAttribute]

public bool canSubmerge;

}

This would yield the following XML document (note the opening <JamesBondCar> element):

<?xml version="1.0" encoding="utf-8"?>

<JamesBondCar xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"

canFly="true" canSubmerge="false" xmlns="http://www.intertechtraining.com">

...

</JamesBondCar>

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

555

Of course, there are numerous other attributes that can be used to control how the XmlSerializer generates the resulting XML document. If you wish to see all of your options, look up the System.Xml. Serialization namespace using the .NET Framework 2.0 SDK documentation.

Persisting Collections of Objects

Now that you have seen how to persist a single object to a stream, let’s examine how to save a set of objects. As you may have noticed, the Serialize() method of the IFormatter interface does not provide a way to specify an arbitrary number of objects (only a single System.Object). On a related note, the return value of Deserialize() is, again, a single System.Object:

public interface IFormatter

{

...

object Deserialize(System.IO.Stream serializationStream);

void Serialize(System.IO.Stream serializationStream, object graph);

}

Recall that the System.Object in fact represents a complete object graph. Given this, if you pass in an object that has been marked as [Serializable] and contains other [Serializable] objects, the entire set of objects is persisted right away. As luck would have it, most of the types found within the System.Collections and System.Collections.Generic namespaces have already been marked as

[Serializable]. Therefore, if you wish to persist a set of objects, simply add the set to the container (such as an ArrayList or List<>) and serialize the object to your stream of choice.

Assume you have updated the JamesBondCar class with a two-argument constructor to set a few pieces of state data (note that you add back the default constructor as required by the XmlSerializer):

[Serializable,

XmlRoot(Namespace = "http://www.intertechtraining.com")] public class JamesBondCar : Car

{

public JamesBondCar(bool skyWorthy, bool seaWorthy)

{

canFly = skyWorthy; canSubmerge = seaWorthy;

}

// The XmlSerializer demands a default constructor! public JamesBondCar(){}

...

}

With this, you are not able to persist any number of JamesBondCars as so:

static void Main(string[] args)

{

...

// Now persist a List<> of JamesBondCars.

List<JamesBondCar> myCars = new List<JamesBondCar>(); myCars.Add(new JamesBondCar(true, true)); myCars.Add(new JamesBondCar(true, false)); myCars.Add(new JamesBondCar(false, true)); myCars.Add(new JamesBondCar(false, false));

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

556 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

xmlFormat = new XmlSerializer(typeof(List<JamesBondCar>),

new Type[] { typeof(JamesBondCar), typeof(Car), typeof(Radio) }); xmlFormat.Serialize(fStream, myCars);

fStream.Close();

Console.ReadLine();

}

Again, because you made use of the XmlSerializer, you are required to specify type information for each of the subobjects within the root object (which in this case is the ArrayList). Had you made use of the BinaryFormatter or SoapFormatter type, the logic would be even more straightforward, for example:

static void Main(string[] args)

{

...

// Save ArrayList object (myCars) as binary.

List<JamesBondCar> myCars = new List<JamesBondCar>();

...

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

FileMode.Create, FileAccess.Write, FileShare.None); binFormat.Serialize(fStream, myCars);

fStream.Close();

Console.ReadLine();

}

Excellent! At this point, you should see how you can use object serialization services to simplify the process of persisting and resurrecting your application’s data. Next up, allow me to illustrate how you can customize the default serialization process.

Source Code The SimpleSerialize application is located under the Chapter 17 subdirectory.

Customizing the Serialization Process

In a vast majority of cases, the default serialization scheme provided by the .NET platform will be exactly what you require. Simply apply the [Serializable] attribute and pass the object graph to your formatter of choice. In some cases, however, you may wish to become more involved with how an object graph is handled during the serialization process. For example, maybe you have a business rule that says all field data must be persisted in uppercase format, or perhaps you wish to add additional bits of data to the stream that do not directly map to fields in the object being persisted (time stamps, unique identifiers, or whatnot).

When you wish to become more involved with the process of object serialization, the System. Runtime.Serialization namespace provides several types that allow you to do so. Table 17-2 describes some of the core types to be aware of.

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

557

Table 17-2. System.Runtime.Serialization Namespace Core Types

Type

Meaning in Life

ISerializable

As of .NET 1.1, implementing this interface was the preferred way to

 

perform custom serialization. As of .NET 2.0, the preferred way to

 

customize the serialization process is to apply a new set of attributes

 

(described in just a bit).

ObjectIDGenerator

This type generates IDs for members in an object graph.

OnDeserializedAttribute

This .NET 2.0 attribute allows you to specify a method that will be

 

called immediately after the object has been deserialized.

OnDeserializingAttribute This .NET 2.0 attribute allows you to specify a method that will be called during the deserialization process.

OnSerializedAttribute

This .NET 2.0 attribute allows you to specify a method that will be

 

called immediately after the object has been serialized.

OnSerializingAttribute

This .NET 2.0 attribute allows you to specify a method that will be

 

called during the serialization process.

OptionalFieldAttribute

This .NET 2.0 attribute allows you to define a field on a type that can be

 

missing from the specified stream.

SerializationInfo

In essence, this class is a “property bag” that maintains name/value

 

pairs representing the state of an object during the serialization

 

process.

 

 

A Deeper Look at Object Serialization

Before we examine various ways in which you can customize the serialization process, it will be helpful to take a deeper look at what takes place behind the scenes. When the BinaryFormatter serializes an object graph, it is in charge of transmitting the following information into the specified stream:

The fully qualified name of the objects in the graph (e.g., MyApp.JamesBondCar)

The name of the assembly defining the object graph (e.g., MyApp.exe)

An instance of the SerializationInfo class that contains all stateful data maintained by the members in the object graph

During the deserialization process, the BinaryFormatter uses this same information to build an identical copy of the object, using the information extracted from the underlying stream.

Note Recall that the SoapFormatter and XmlSerializer do not persist a type’s fully qualified name or the name of the defining assembly. These types are concerned only with persisting exposed field data.

The big picture can be visualized as shown in Figure 17-3.

558 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

Figure 17-3. The serialization process

Beyond moving the required data into and out of a stream, formatters also analyze the members in the object graph for the following pieces of infrastructure:

A check is made to determine whether the object is marked with the [Serializable] attribute. If the object is not, a SerializationException is thrown.

If the object is marked [Serializable], a check is made to determine if the object implements the ISerializable interface. If this is the case, GetObjectData() is called on the object.

If the object does not implement ISerializable, the default serialization process is used, serializing all fields not marked as [NonSerialized].

In addition to determining if the type supports ISerializable, formatters (as of .NET 2.0) are also responsible for discovering if the types in question support members that have been adorned with the [OnSerializing], [OnSerialized], [OnDeserializing], or [OnDeserialized] attribute. We’ll examine the role of these attributes in just a bit, but first let’s look at the role of ISerializable.

Customizing Serialization Using ISerializable

Objects that are marked [Serializable] have the option of implementing the ISerializable interface. By doing so, you are able to “get involved” with the serialization process and perform any preor post-data formatting. This interface is quite simple, given that it defines only a single method,

GetObjectData():

//When you wish to tweak the serialization process,

//implement ISerializable.

public interface ISerializable

{

void GetObjectData(SerializationInfo info, StreamingContext context);

}

The GetObjectData() method is called automatically by a given formatter during the serialization process. The implementation of this method populates the incoming SerializationInfo parameter with a series of name/value pairs that (typically) map to the field data of the object being persisted. SerializationInfo defines numerous variations on the overloaded AddValue() method, in addition to a small set of properties that allow the type to get and set the type’s name, defining assembly, and member count. Here is a partial snapshot:

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

559

public sealed class SerializationInfo : object

{

public SerializationInfo(Type type, IFormatterConverter converter); public string AssemblyName { get; set; }

public string FullTypeName { get; set; } public int MemberCount { get; }

public void AddValue(string name, short value); public void AddValue(string name, UInt16 value); public void AddValue(string name, int value);

...

}

Types that implement the ISerializable interface must also define a special constructor taking the following signature:

//You must supply a custom constructor with this signature

//to allow the runtime engine to set the state of your object.

[Serializable]

class SomeClass : ISerializable

{

private SomeClass (SerializationInfo si, StreamingContext ctx) {...}

...

}

Notice that the visibility of this constructor is set as private. This is permissible given that the formatter will have access to this member regardless of its visibility. These special constructors tend to be marked as private to ensure that the casual object user would never create an object in this manner. As you can see, the first parameter of this constructor is an instance of the SerializationInfo type (seen previously).

The second parameter of this special constructor is a StreamingContext type, which contains information regarding the source or destination of the bits. The most informative member of this type is the State property, which represents a value from the StreamingContextStates enumeration. The values of this enumeration represent the basic composition of the current stream.

To be honest, unless you are implementing some low-level custom remoting services, you will seldom need to deal with this enumeration directly. Nevertheless, here are the possible names of the StreamingContextStates enum (consult the .NET Framework 2.0 SDK documentation for full details):

public enum StreamingContextStates

{

CrossProcess,

CrossMachine,

File,

Persistence,

Remoting,

Other,

Clone,

CrossAppDomain, All

}

To illustrate customizing the serialization process using ISerializable, assume you have

a class type that defines two points of string data. Furthermore, assume that you must ensure the string values are serialized to the stream in all uppercase and deserialized from the stream in all lowercase. To account for such rules, you could implement ISerializable as so (be sure to “use” the

System.Runtime.Serialization namespace):

560 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]

class MyStringData : ISerializable

{

public string dataItemOne, dataItemTwo;

public MyStringData(){}

private MyStringData(SerializationInfo si, StreamingContext ctx)

{

// Rehydrate member variables from stream. dataItemOne = si.GetString("First_Item").ToLower(); dataItemTwo = si.GetString("dataItemTwo").ToLower();

}

void ISerializable.GetObjectData(SerializationInfo info, StreamingContext ctx)

{

// Fill up the SerializationInfo object with the formatted data. info.AddValue("First_Item", dataItemOne.ToUpper()); info.AddValue("dataItemTwo", dataItemTwo.ToUpper());

}

}

Notice that when you are filling the SerializationInfo type from within the GetObjectData() method, you are not required to name the data points identically to the type’s internal member variables. This can obviously be helpful if you need to further decouple the type’s data from the persisted format. Do be aware, however, that you will need to obtain the values from within the private constructor using the same names assigned within GetObjectData().

To test your customization, assume you have persisted an instance of MyStringData using

a SoapFormatter. When you view the resulting *.soap file, you will note that the string fields have indeed been persisted in uppercase:

<SOAP-ENV:Envelope xmlns:xsi="..."> <SOAP-ENV:Body>

<a1:MyStringData id="ref-1" xmlns:a1="...">

<First_Item id="ref-3">THIS IS SOME DATA.</First_Item> <dataItemTwo id="ref-4">HERE IS SOME MORE DATA</dataItemTwo>

</a1:MyStringData> </SOAP-ENV:Body>

</SOAP-ENV:Envelope>

Customizing Serialization Using Attributes

Although implementing the ISerializable interface is still possible under .NET 2.0, the preferred manner to customize the serialization process is to define methods that are attributed with any of the new serialization-centric attributes ([OnSerializing], [OnSerialized], [OnDeserializing], or

[OnDeserialized]). Using these attributes is less cumbersome than implementing ISerializable, given that you do not need to manually interact with an incoming SerializationInfo parameter. Instead, you are able to directly modify your state data while the formatter is operating on the type. When applying these attributes, the methods must be defined to receive a StreamingContext parameter and return nothing (otherwise, you will receive a runtime exception). Do note that you are not required to account for each of the serialization-centric attributes, and you can simply con-

tend with the stages of serialization you are interested in intercepting. To illustrate, here is a new [Serializable] type that has the same requirements as MyStringData, this time accounted for using the [OnSerializing] and [OnDeserialized] attributes:

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

561

[Serializable] class MoreData

{

public string dataItemOne, dataItemTwo;

[OnSerializing]

internal void OnSerializing(StreamingContext context)

{

// Called during the serialization process. dataItemOne = dataItemOne.ToUpper(); dataItemTwo = dataItemTwo.ToUpper();

}

[OnDeserialized]

internal void OnDeserialized(StreamingContext context)

{

// Called once the deserialization process is complete. dataItemOne = dataItemOne.ToLower();

dataItemTwo = dataItemTwo.ToLower();

}

}

If you were to serialize this new type, you would again find that the data has been persisted as uppercase and deserialized as lowercase.

Source Code The CustomSerialization project is included under the Chapter 17 subdirectory.

Versioning Serializable Objects

To wrap up this chapter, the final topic to address is the process of versioning serializable objects. To understand why this may be necessary, consider the following scenario. Assume you have created the UserPrefs class (mentioned at the beginning of the chapter) as so:

[Serializable] class UserPrefs

{

public string objVersion = "1.0"; public ConsoleColor BackgroundColor; public ConsoleColor ForegroundColor;

public UserPrefs()

{

BackgroundColor = ConsoleColor.Black; ForegroundColor = ConsoleColor.Red;

}

}

Now, assume you have an application that serializes an instance of this class using a

BinaryFormatter:

static void Main(string[] args)

{

UserPrefs up = new UserPrefs(); up.BackgroundColor = ConsoleColor.DarkBlue; up.ForegroundColor = ConsoleColor.White;

562 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

// Save an instance of UserPrefs to file.

BinaryFormatter binFormat = new BinaryFormatter(); Stream fStream = new FileStream(@"C:\user.dat",

FileMode.Create, FileAccess.Write, FileShare.None); binFormat.Serialize(fStream, up);

fStream.Close();

Console.ReadLine();

}

At this point, an instance of UserPrefs (version 1.0) has been persisted to C:\user.dat. Now, what if you updated the definition of UserPrefs class with two new fields:

[Serializable] class UserPrefs

{

public string objVersion = "2.0"; public ConsoleColor BackgroundColor; public ConsoleColor ForegroundColor;

// New!

public int BeepFreq; public string ConsoleTitle;

public UserPrefs()

{

BeepFreq = 1000; ConsoleTitle = "My Console";

BackgroundColor = ConsoleColor.Black; ForegroundColor = ConsoleColor.Red;

}

}

Imagine this same application now attempts to deserialize the instance of the persisted UserPrefs object version 1.0 as so (note the previous serialization logic has been removed in order for this example to work):

static void Main(string[] args)

{

// Load an instance of UserPrefs (1.0) to memory?

UserPrefs up = null;

BinaryFormatter binFormat = new BinaryFormatter(); Stream fStream = new FileStream(@"C:\user.dat",

FileMode.Open, FileAccess.Read, FileShare.None); up = (UserPrefs)binFormat.Deserialize(fStream); fStream.Close();

Console.ReadLine();

}

You will find a runtime exception is thrown:

Unhandled Exception: System.Runtime.Serialization.SerializationException: Member 'BeepFreq' in class ' VersionedObject.UserPrefs' is not present in the serialized stream and is not marked with System.Runtime.Serialization.OptionalFieldAttribute.

The problem is that the original UserPrefs object persisted to C:\user.dat did not have storage for the two new fields found in your updated class definition (BeepFreq and ConsoleTitle). Clearly, this is problematic, as it is quite natural for a serialized object to evolve over its lifetime.

Prior to .NET 2.0, the only way to account for the possibility that previously persisted objects may not have each and every field of the latest and greatest version of the class was to implement

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

563

ISerializable and take matters into your own hands. However, as of .NET 2.0, new fields can now be explicitly marked with the [OptionalField] attribute (found within the System.Runtime.Serialization namespace):

[Serializable] class UserPrefs

{

public ConsoleColor BackgroundColor; public ConsoleColor ForegroundColor;

// New! [OptionalField] public int BeepFreq;

[OptionalField]

public string ConsoleTitle;

public UserPrefs()

{

BeepFreq = 1000; ConsoleTitle = "My Console";

BackgroundColor = ConsoleColor.Black; ForegroundColor = ConsoleColor.Red;

}

}

When a formatter deserializes an object that does not contain fields such optional fields, it will no longer throw a runtime exception. Rather, the data that is preserved is mapped back into the existing fields (BackgroundColor and ForegroundColor, in this case), while the remaining fields are simply assigned their default values.

Note Understand that the use of [OptionalField] does not completely solve the process of versioning persisted objects. However, this attribute does provide a workaround for the most common headache of the versioning process (adding new field data). More elaborate versioning tasks may still require implementing the

ISerializable interface.

Source Code The VersionedObject project is included under the Chapter 17 subdirectory.

Summary

This chapter introduced the topic of object serialization services. As you have seen, the .NET platform makes use of an object graph to correctly account for the full set of related objects that are to be persisted to a stream. As long as each member in the object graph has been marked with the [Serializable] attribute, the data is persisted using your format of choice (binary, SOAP, or XML).

You also learned that it is possible to customize the out-of-the-box serialization process using two possible approaches. First, you learned how to implement the ISerializable interface (and support a special private constructor) to become more involved with how formatters persist the supplied data. Next, you came to know a set of new attributes introduced with .NET 2.0, which simplifies the process of custom serialization. Just apply the [OnSerializing], [OnSerialized], [OnDeserializing], or [OnDeserialized] attribute on members taking a StreamingContext parameter, and the formatters will invoke them accordingly. The chapter wrapped up with an examination of a final attribute, [OptionalField], which can be used to gracefully version a serializable type.