Serialization Formatters

Serialization Formatters

As demonstrated earlier, serialization formatters serialize objects to streams. What we haven t discussed yet is how to write a custom serialization formatter that can be plugged into .NET Remoting. Writing a serialization formatter is largely an exercise in the following tasks, in no particular order:

  • Obtaining a list of an object type s serializable members

  • Traversing an object graph that s rooted at the object being serialized

  • Serializing the full type name of an object, its containing assembly, and the values of its serializable members

  • Serializing references to objects within the graph so that the graph can be reconstructed during deserialization

Fortunately, the .NET Framework provides several classes that you can use to facilitate coding solutions for each of these tasks. Table 8-3 lists the classes provided by the .NET Framework and the purpose each serves. We ll look at examples of using these classes shortly when we look at performing each of the tasks.

Table 8-3. Classes Useful for Writing Serialization Formatters

Class

Namespace

Description

FormatterServices

System.Runtime.Serialization

Serialization/deserialization

ObjectIDGenerator

System.Runtime.Serialization

Serialization

ObjectManager

System.Runtime.Serialization

Deserialization

Formatter

System.Runtime.Serialization

Can be used as base class for a formatter; provides methods helpful for object graph traversal and object scheduling

Let s examine using these classes in isolation first, to see what they can do. We ll put them all together when we write a custom formatter later in the section.

Obtaining a Type s Serializable Members

Table 8-4 lists some of the methods that the FormatterServices class provides that facilitate writing custom serialization formatters.

Table 8-4. Significant Public Methods of System.Runtime. Serialization.FormatterServices

Method

Description

GetSerializableMembers

Obtains the serializable members for a given type

GetObjectData

Obtains the values for one or more serializable members of an object instance

GetUninitializedObject

Obtains an uninitialized object instance during deserialization

PopulateObjectMembers

Initializes an uninitialized object instance s members with values

In an example in the previous section, we defined the SomeClass2 type. The following code snippet demonstrates using each of the FormatterServices methods listed in Table 8-4 to obtain the serializable members and their values for a serializable instance of the SomeClass2 type:

SomeClass2 sc = new SomeClass2(); // Obtain the serializable members and their values. MemberInfo[] mi = FormatterServices.GetSerializableMembers(sc.GetType()); object[] vals = FormatterServices.GetObjectData(sc,mi); // Obtain an uninitialized object and populate its members. SomeClass2 sc2 = (SomeClass2)FormatterServices.GetUninitializedObject(typeof (SomeClass2)); FormatterServices.PopulateObjectMembers(sc2,mi,vals);

The GetSerializableMembers method returns an array of System.Reflection.MemberInfo instances for a specified type. If you pass a type that isn t serializable to GetSerializableMembers, the common language runtime will raise a System.Runtime.Serialization.SerializationException exception. Note that passing a type implementing the ISerializable interface to the GetSerializableMembers method doesn t result in the GetSerializableMembers method calling ISerializable.GetObjectData. This means that you might not actually get all the serialization members that the type implementer intended.

To obtain the values for each serializable member, you pass the MemberInfo array to the GetObjectData method, which returns an object array whose elements correspond to the values for the serializable members. The two arrays are populated so that the ith element of the object array is the value of the member defined by the ith element in the MemberInfo array.

To reverse the process we ve just described, you create an uninitialized instance of the SomeClass2 type by using FormatterServices.GetUninitializedObject. The critical word here is uninitialized the constructor isn t called, and members that reference other objects are set to null or 0. To initialize the uninitialized object instance, you use the PopulateObjectMembers method, passing it the uninitialized object instance, the MemberInfo array describing each member you are initializing, and a matching object array with the values for the members. The return value of PopulateObjectMembers is the object being populated.

Traversing an Object Graph

The object being serialized corresponds to the root node in the object graph. All other nodes in the graph represent an object that s reachable from the object being serialized, either directly as a member or indirectly via a member of a referenced object. The basic procedure to traverse an object graph for serialization is to start at the root object and obtain its serializable members. Then you traverse each serializable member s object graph and so forth until all nodes in the graph have been traversed. For acyclic object graphs, such as the one shown in Figure 8-2, the exercise is fairly simple.

figure 8-2 an acyclic object graph

Figure 8-2. An acyclic object graph

However, some object graphs might contain cycles. A cycle occurs when one object references another object that directly or indirectly references the original object. Cycles are problematic because if you don t detect them, you ll end up traversing the cycle forever. Figure 8-3 shows an abstract view of an object graph that contains a cycle.

figure 8-3 an object graph that contains a cycle

Figure 8-3. An object graph that contains a cycle

Identifying Objects by Using the ObjectIDGenerator Class

As the formatter traverses the object graph, it assigns each object an identifier that it can use when serializing objects that reference the object being serialized. The formatter does this by using the ObjectIDGenerator class, which keeps track of all objects encountered during traversal. You use the ObjectIDGenerator.GetID method to obtain a long value that identifies the object instance passed to the GetID method. The GetID method takes two parameters. The first parameter is an object instance for which GetID should obtain the identifier. The second parameter is a Boolean parameter that indicates whether this is the first time the object instance has been passed to the GetID method. The following code snippet demonstrates how to use the ObjectIDGenerator class:

ObjectIDGenerator idgen = new ObjectIDGenerator(); SomeClass sc = new SomeClass(); bool firstTime = false; // id_sc == 0, firstTime == true on return long id_sc = idgen.HasId(sc, out firstTime); // id_sc == 1, firstTime == true on return id_sc = idgen.GetId(sc, out firstTime); // id_sc == 1, firstTime == false on return id_sc = idgen.HasId(sc, out firstTime); // id_sc == 1, firstTime == false on return id_sc = idgen.GetId(sc, out firstTime);

Scheduling Objects for Serialization

The .NET Framework uses a technique known as scheduling to help serialize an object graph. While traversing an object graph, if the formatter encounters an object instance (either the root object instance or a member of an object that references another object instance), it performs the following actions:

  1. Obtains an identifier for the object instance from the ObjectIDGenerator class

  2. Serializes a reference to the object instance by using the object identifier rather than serializing the object instance itself

  3. Schedules the object instance for later serialization by placing it in a queue of objects waiting to be serialized

So far, we ve discussed using the FormatterServices class to obtain an object instance s serializable members, traversing an object graph, and using the ObjectIDGenerator class. We ll use each of these tasks shortly to implement a custom formatter s IFormatter.Serialize method. But before we do that, let s look at how we can use the ObjectManager class to aid in deserialization.

Using the ObjectManager Class

The ObjectManager class allows you to construct an object graph from scratch. If you have all the object instances in a graph and know how they relate to one another, you can use the ObjectManager class to construct the object graph. Table 8-5 lists the useful members of the ObjectManager class.

Table 8-5. Significant Public Methods of System.Runtime. Serialization.ObjectManager

Method

Description

DoFixups

Call after all objects in the graph have been registered with the ObjectManager. When this function returns, all outstanding object references have been fixed up.

GetObject

Obtains a registered object instance by its identifier.

RaiseDeserializationEvent

Raises the deserialization event.

RecordArrayElementFixup

Records an array element fixup when an array element references another object instance in the graph.

RecordDelayedFixup

Records a fixup for members of a SerializationInfo instance associated with a registered object.

RecordFixup

Records a fixup for members of an object instance.

RegisterObject

Registers an object instance with its identifier.

In general, you use the ObjectManager to reconstruct an object graph by performing the following tasks:

  1. Register object instances and their identifiers with the ObjectManager via the RegisterObject method.

  2. Record fixups that map members of object instances to other object instances in the graph via the object identifiers by using the RecordFixup, RecordArrayElementFixup, and RecordDelayedFixup methods.

  3. Instruct the ObjectManager to perform the recorded fixups via the DoFixups method.

  4. Query the ObjectManager for the root object in the graph via the GetObject method.

The following code uses the ObjectManager class to reconstruct the object graph depicted in Figure 8-1:

// Create an ObjectManager instance. ObjectManager om = new ObjectManager(new SurrogateSelector(), new StreamingContext(StreamingContextStates.All)); // Set up object 1, the root object. object sc3 = FormatterServices.GetUninitializedObject(typeof(SomeClass3)); // Register object 1. om.RegisterObject(sc3, 1); // Set up to initialize the members of object 1. System.Reflection.MemberInfo[] mi = FormatterServices.GetSerializableMembers(typeof(SomeClass3)); // Record a fixup for the first member of object 1 to reference // object 2. om.RecordFixup(1,mi[0],2); // Initialize the second member of object 1 using FormatterServices. FormatterServices.PopulateObjectMembers( sc3, new MemberInfo[]{ mi[1] }, new Object[]{ 2112 }); // Set up object 2, the instance of SomeClass2. SerializationInfo info = new SerializationInfo( typeof(SomeClass2), new FormatterConverter()); info.AddValue("m1", 4000); info.AddValue("m2", 4001); // Record a delayed fixup for the TimeStamp member // for object 2 referencing object 3. om.RecordDelayedFixup(2, "TimeStamp", 3); // Register object 2 and its associated SerializationInfo instance. object sc2 = FormatterServices.GetUninitializedObject(typeof(SomeClass2)); om.RegisterObject(sc2, 2, info); // Register object 3, the TimeStamp value -technically not // part of the object graph, but required because SomeClass2 expects // the TimeStamp value to be in the SerializationInfo. om.RegisterObject(DateTime.Now,3); // The ObjectManager now has enough information to // construct the object graph--perform the fixups. om.DoFixups(); // Obtain the root object. SomeClass3 _sc3 = (SomeClass3)om.GetObject(1);

The code begins by creating an instance of the ObjectManager class. The root object in the graph is an instance of the SomeClass3 class. We create an uninitialized instance of the SomeClass3 class by using the FormatterServices.GetUninitialized method, which we then register with the ObjectManager, assigning it an object identifier of 1. The next step is to initialize the members of object 1. The m_sc2 member of the SomeClass3 class references an instance of the SomeClass2 class. Because we don t yet have an object instance of type SomeClass2, we can t immediately initialize the m_sc2 member. Therefore, we need to record a fixup for the m_sc2 member for object 1 so that it references the object instance that has an identifier of 2. The fixup instructs the ObjectManager to set the m_sc2 member equal to the object instance that has an object identifier of 2 during the fixup stage when we call the DoFixups method. The second member of the SomeClass3 class is an integer value and can be initialized immediately by using the FormatterServices.PopulateObjectMembers method.

Next we set up the second object in the object graph, an instance of SomeClass2. Because SomeClass2 implements ISerializable, we create a new instance of SerializationInfo, which we immediately populate with the two integer members, m1 and m2. The SomeClass2 implementation of ISerializable.GetObjectData expects a third member to be present in the SerializationInfo, TimeStamp. To demonstrate using delayed fixups, we call the RecordDelayedFixup method to record a delayed fixup that associates the TimeStamp member of object 1 with object 3, which we haven t yet registered. The RecordDelayedFixup method defers initialization of a SerializationInfo member that references an object that hasn t yet been registered until the required object is registered with the ObjectManager. After recording the delayed fixup, we register the uninitialized instance of SomeClass2 and its associated SerializationInfo with the ObjectManager, assigning it an object ID of 2. At this point, the SerializationInfo contains two members, m1 and m2, and their corresponding values. Because of the delayed fixup, when we register an instance of DateTime as object ID 3, the ObjectManager adds a member named TimeStamp to the SerializationInfo for object 2. The TimeStamp member references the object instance corresponding to object 3.

Next we call the DoFixups method on the ObjectManager instance. This causes the ObjectManager to iterate over its internal data structures, performing any outstanding fixups. For member fixups, the ObjectManager initializes the referring member with a reference to the actual object instance. For delayed fixups of objects that don t have a serialization surrogate, the ObjectManager invokes the object s ISerializable.GetObjectData method, passing it the SerializationInfo instance associated with it when the object was registered. If the object does have a serialization surrogate, the ObjectManager invokes the surrogate s ISerializationSurrogate.SetObjectData method, passing it the SerializationInfo instance associated with it when the object was registered. For array fixups, the ObjectManager initializes the array element with a reference to the actual object instance. Once the fixups are complete, we request the object instances by their identifier numbers.

Using the Formatter Class

The .NET Framework includes a class named System.Runtime.Serialization.Formatter that you can use as a base class when writing custom formatters. Table 8-6 lists the significant public members of the Formatter class. The Schedule and GetNext methods implement the scheduling technique for object serialization that we described earlier in the section. When you want to schedule an object instance for later serialization, you call the Schedule method, passing the object instance as a parameter. The Schedule method obtains and returns the object identifier assigned by the ObjectIDGenerator referenced by the Formatter instance s m_idGenerator member. Prior to returning, the Schedule method enqueues the object instance in the queue referenced by the m_objectQueue member. The GetNext method dequeues and returns the next object in the queue referenced by the m_objectQueue member.

Table 8-6. Significant Public Members of System.Runtime. Serialization.Formatter

Member

Member Type

Description

m_idGenerator

Protected field

A reference to the ObjectIDGenerator instance used to identify objects during traversal of the object graph.

m_objectQueue

Protected field

A queue of objects waiting to be serialized.

Schedule

Method

Schedules an object instance for later serialization. The return value indicates the object ID assigned to the object instance being scheduled.

GetNext

Method

Obtains the next object instance to be serialized.

WriteMember

Method

Serializes a member of an object instance. The method invokes one of the various WriteXXXX members based on the object s type.

WriteObjectRef

Method

Override this virtual method to write an object reference instead of the actual object instance. Most formatters simply write the object ID to the stream.

WriteValueType

Method

Override this virtual method to write a ValueType to the stream. For primitive types, override the WriteXXXX methods. WriteMember will call this method if the ValueType isn t a primitive type.

WriteXXXX

Method

Any one of the many methods named after the type they write WriteByte, WriteInt32, WriteDateTime, and so on.

Implementing a Custom Serialization Formatter

Now that we ve discussed the more significant classes that the .NET Framework provides to facilitate developing a custom serialization formatter, let s put what we ve learned to use. In doing so, we ll follow these steps:

  1. Define a serialization format.

  2. Implement IFormatter.Serialize.

  3. Implement IFormatter.Deserialize.

Defining a Serialization Format

As mentioned at various points throughout this book, the SoapFormatter serializes object graphs by using a SOAP format. Likewise, the BinaryFormatter serializes object graphs by using an efficient binary format. Before developing our formatter, we need a format to implement. To facilitate explaining and demonstrating the principles required to implement a custom formatter, we ll use a human-readable format that consists of field tags followed by the string representation of their values. Each field tag delimits a specific type of information used to reconstruct the object graph. Table 8-7 shows the field tags we ll use in our formatter.

Table 8-7. A Custom Serialization Format

Field Tag

Value Meaning

o_id:

The object s ID as assigned by the ObjectIDGenerator.

o_assembly:

The object s full assembly name, including version, culture info, and public key token.

o_type:

The object s fully qualified type name.

m_count:

Present when serialized members are members of a SerializationInfo. Number of members serialized for the current object.

m_name:

Member name.

m_type:

Member fully qualified type name.

m_value:

Member value in string form.

m_value_type:

Indicates that the value for this member is actually a type. We serialize the name of the type rather than the Type instance.

m_value_refid:

Indicates that the value refers to another object in the graph by its object identifier.

array_rank:

Number of dimensions in the array.

array_length:

Length of a dimension in the array.

array_lowerbound:

Lower bound in a dimension in the array.

Here s an example of a serialized object graph produced by using the custom formatter we ll develop in this section:

o_id:1 o_assembly:Serialization, Version=1.0.912.37506, Culture=neutral, PublicKeyToken=null o_type:Serialization.TestClassA m_count:2 m_name:__v1 m_type:System.DateTime m_value:7/1/2002 9:50:24 PM m_name:__v2 m_type:Serialization.TestClassB m_value_refid:2 o_id:2 o_assembly:Serialization, Version=1.0.912.37506, Culture=neutral, PublicKeyToken=null o_type:Serialization.TestClassB m_name:m_a m_type:Serialization.TestClassA m_value_refid:1

As you can see, each field tag and its associated value appear on a single line. This has one undesirable implication: values cannot contain carriage-return/linefeed characters. If we want to use this formatter in a production setting, we ll definitely need to address that issue, but because we re writing this formatter for demonstration purposes, we won t be concerned with this.

The first three lines begin the object serialization information by indicating the object identifier, full assembly name, and type name. The following line begins with m_count and indicates that two members are serialized for this object. The presence of the m_count field tag indicates that the following members should be placed in a SerializationInfo during deserialization. The name, type, and value for each member follow. Notice that the second member, named __v2, is a reference to an object with an identifier equal to 2. The next three lines begin a new object with identifier equal to 2. This object has only one member, and its name, type, and value indicate that the member references the object with an identifier equal to 1.

The following code defines a class named FieldNames, which we ll use to help implement the custom formatter:

class FieldNames { // Object manager ID public static string OBJECT_ID = "o_id:"; // Assembly name public static string OBJECT_ASSEMBLY = "o_assembly:"; // Object type public static string OBJECT_TYPE = "o_type:"; // Number of members in this object public static string MEMBER_COUNT = "m_count:"; // Member name public static string MEMBER_NAME = "m_name:"; // Member type public static string MEMBER_TYPE = "m_type:"; // Member value public static string MEMBER_VALUE = "m_value:"; // Member value is a type public static string MEMBER_VALUE_TYPE = "m_value_type:"; // Object manager ID public static string OBJECT_REFID = "m_value_refid:"; // Number of dimensions public static string ARRAY_RANK = "array_rank:"; // Length of a dimension public static string ARRAY_LENGTH = "array_length:"; // Lower bound of a dimension public static string ARRAY_LOWERBOUND = "array_lowerbound:"; // Special null-value indicator public static string MEMBER_VALUE_NULL = "m_value_null"; public static long ParseObjectRefID(string s) { return Convert.ToInt64(s.Substring(OBJECT_REFID.Length)); } public static long ParseObjectID(string s) { return Convert.ToInt64(s.Substring(OBJECT_ID.Length)); } public static string ParseObjectAssembly(string s) { return s.Substring(OBJECT_ASSEMBLY.Length); } public static string ParseObjectType(string s) { return s.Substring(OBJECT_TYPE.Length); } public static long ParseMemberCount(string s) { return Convert.ToInt64(s.Substring(MEMBER_COUNT.Length)); } public static string ParseMemberName(string s) { return s.Substring(MEMBER_NAME.Length); } public static string ParseMemberType(string s) { return s.Substring(MEMBER_TYPE.Length); } public static string ParseMemberValue(string s) { return s.Substring(MEMBER_VALUE.Length); } public static string ParseMemberValueType(string s) { return s.Substring(MEMBER_VALUE_TYPE.Length); } public static long ParseArrayRank(string s) { return Convert.ToInt64(s.Substring(ARRAY_RANK.Length)); } public static long ParseArrayLength(string s) { return Convert.ToInt64(s.Substring(ARRAY_LENGTH.Length)); } public static long ParseArrayLowerBound(string s) { return Convert.ToInt64(s.Substring( ARRAY_LOWERBOUND.Length)); } }

The FieldNames class simply defines a static member for each of the field tags listed in Table 8-7. In addition, the FieldNames class defines ParseXXXX methods that parse each field tag. We ll use the ParseXXXX methods when we implement the IFormatter.Deserialize method.

Implementing the IFormatter Interface

Serialization formatters are classes that implement the IFormatter interface. Table 8-8 lists the members defined by the IFormatter interface.

Table 8-8. Members of System.Runtime.Serialization.IFormatter

Member

Member Type

Description

Binder

Read-write property

Allows equipping the formatter instance with a SerializationBinder instance to deserialize a serialized type to a different type

Context

Read-write property

Can reference a StreamingContext instance that contains information about the serialization or deserialization operation

SurrogateSelector

Read-write property

Can reference an instance of a surrogate selector class

Deserialize

Method

Deserializes an object graph from a stream

Serialize

Method

Serializes an object graph to a stream

The following code listing begins implementing the MyFormatter class, which we ll fully implement and explain in the next few sections:

public class MyFormatter : Formatter { SerializationBinder _binder; StreamingContext _streamingcontext; ISurrogateSelector _surrogateselector; StreamWriter _writer; StreamReader _reader; ObjectManager _om; public MyFormatter() { } public override SerializationBinder Binder { get { return _binder; } set { _binder = value; } } public override StreamingContext Context { get { return _streamingcontext; } set { _streamingcontext = value; } } public override ISurrogateSelector SurrogateSelector { get { return _surrogateselector; } set { _surrogateselector = value; } }

The MyFormatter class derives from the System.Runtime.Serialization.Formatter class to leverage the object graph traversal functionality, which we explained earlier in the section. Along with the members implementing the IFormatter properties, we ve defined a StreamWriter member named _writer that we ll use for serialization. For deserialization, we ve defined a StreamReader member named _reader and an ObjectManager member named _om.

Implementing the IFormatter.Serialize Method

The function of the IFormatter.Serialize method is to serialize an object graph to a stream by using a specific format to lay out the serialization information. Our implementation of IFormatter.Serialize follows:

public override void Serialize(Stream serializationStream, object graph) { _writer = new StreamWriter(serializationStream); // Schedule the top object. Schedule(graph); // Get the next object to be serialized and serialize it. object oTop; long topId; while( (oTop = GetNext(out topId)) != null ) { // Execution of the WriteObject method will likely result // in the scheduling of more objects for serialization. WriteObject(oTop, topId); } _writer.Flush(); }

To start the serialization process, the IFormatter.Serialize method passes the root object of the object graph by calling Formatter.Schedule, passing the graph as the parameter. Schedule will obtain an identifier for the root object, which should be 1, and enqueue it for later serialization. To continue the serialization process, we call the Formatter.GetNext method to retrieve the next object in the serialization queue. Assuming GetNext returns an object instance rather than null, we pass that object instance and its identifier to the MyFormatter.WriteObject method. The process continues until GetNext returns null, indicating that no more objects to serialize exist. The implementation of the WriteObject method follows:

private void WriteObject(object obj, long objId) { // Write object preamble. WriteField(FieldNames.OBJECT_ID, objId); WriteField(FieldNames.OBJECT_ASSEMBLY, obj.GetType().Assembly); WriteField(FieldNames.OBJECT_TYPE, obj.GetType().FullName); // Don't write members of array and // string types; handle these as special cases. if ( obj.GetType().IsArray ) { WriteArray(obj, "", obj.GetType()); } else if ( obj.GetType() == typeof(string) ) { WriteField(FieldNames.MEMBER_VALUE, obj); } else { // Write object members. WriteObjectMembers(obj, objId); } }

The WriteObject method handles arrays and strings as special cases. For arrays, we don t want to write all the Array class members to the stream. We need to write only the array rank, lower bounds, and length, followed by each array element which is what the WriteArray method does. We ll look at the WriteArray method later in this section. For strings, we just write the string value directly to the stream by using the WriteField method. For any other types, the WriteObject serializes the object instance to the serialization stream by writing the object ID, assembly information, and full type name to the stream by using the WriteField method. The WriteObject method then writes the object instance s serializable members to the stream by using the WriteObjectMembers method.

The implementation of the WriteField method simply writes the field name and the string representation of the value on a single line to the StreamWriter:

// // Write a format field to the stream. void WriteField(string field_name, object oValue) { _writer.WriteLine("{0}{1}", field_name, oValue); }

The implementation of the WriteObjectMembers method is a bit more complex, as the following listing shows:

private void WriteObjectMembers(object obj, long objId) { // See if we need to use a surrogate for this object. ISerializationSurrogate surrogate = null; if ( _surrogateselector != null ) { // Does the surrogate selector have a surrogate // registered for this type? ISurrogateSelector selector; surrogate = _surrogateselector.GetSurrogate( obj.GetType(), _streamingcontext, out selector ); } if ( surrogate != null ) { // Yes, a surrogate is registered; call its GetObjectData // method. SerializationInfo info = new SerializationInfo( obj.GetType(), new FormatterConverter()); surrogate.GetObjectData(obj, info, this._streamingcontext); // Write the serialization info members to the stream. WriteSerializationInfo(info); } else if ( IsMarkedSerializable(obj) && ( obj is ISerializable )) { // The object type implements ISerializable. // Let the object serialize itself // via its GetObjectData method. SerializationInfo info = new SerializationInfo( obj.GetType(), new FormatterConverter()); ((ISerializable)obj).GetObjectData(info, this._streamingcontext); // Write the serialization info members to the stream. WriteSerializationInfo(info); } else if ( IsMarkedSerializable(obj) ) { // The object type does not implement // ISerializable and we have no surrogate for it. WriteSerializableMembers(obj, objId); } else { // The type cannot be serialized; throw an exception. throw new SerializationException(); } } 

The WriteObjectMembers method utilizes several of the techniques for serializing an object that we discussed earlier in the chapter and follows the algorithm that all serialization formatters must follow for serializing object instances. First, the method determines whether the formatter has a surrogate selector. If so, the method asks the surrogate selector whether it has a serialization surrogate to serialize the object with. If so, the WriteObjectMembers method uses the serialization surrogate to serialize the object s members into a new instance of SerializationInfo that it then writes to the stream by calling the WriteSerializationInfo method. If the formatter doesn t have a surrogate selector or no surrogate exists for the object s type, we determine whether the object s type has the SerializableAttribute attribute and implements the ISerializable interface. If the object s type meets these criteria, we allow the object instance to serialize itself into a new instance of SerializationInfo that we then write to the stream by calling the WriteSerializationInfo method. If the object type has the SerializableAttribute attribute but doesn t implement the ISerializable interface, we manually serialize the object s members by using the WriteSerializableMembers method. If none of the other criteria are met, we can t serialize this object, so we throw a SerializationException exception.

The IsMarkedSerializable method inspects the type attributes for the object s Type for the presence of the TypeAttributes.Serializable mask, as follows:

// Is the type attributed with [Serializable]? private bool IsMarkedSerializable(object o) { Type t = o.GetType(); TypeAttributes taSerializableMask = (t.Attributes & TypeAttributes.Serializable); return ( taSerializableMask == TypeAttributes.Serializable ); }

The WriteSerializationInfo method first writes the member count to the stream by using the WriteField method to facilitate deserialization of the SerializationInfo members. After writing the member count, we write each member of the SerializationInfo instance by calling the Formatter.WriteMember method, as the following listing shows:

// Write out all members of the serialization info. private void WriteSerializationInfo(SerializationInfo info) { // Write the member count to the stream. WriteField(FieldNames.MEMBER_COUNT, info.MemberCount); // Write each member of the serialization info to the stream. SerializationInfoEnumerator sie = info.GetEnumerator(); while(sie.MoveNext()) { WriteMember(sie.Name,sie.Value); } }

The WriteSerializableMembers method uses FormatterServices class to obtain the serializable members and values for the object instance, as we did earlier in the chapter. After obtaining the MemberInfo array and object array containing the member values, the method writes each member to the stream by calling the WriteMember method, as the following listing shows.

private void WriteSerializableMembers(object obj, long objId) { System.Reflection.MemberInfo[] mi = FormatterServices.GetSerializableMembers(obj.GetType()); if ( mi.Length > 0 ) { object[] od = FormatterServices.GetObjectData(obj, mi); for( int i = 0; i < mi.Length; ++i ) { WriteMember(mi[i].Name, od[i]); } } }

The Formatter.WriteMember method is a protected virtual method. As shown in Table 8-6, the WriteMember method examines the type of the object passed as the data parameter and, based on the type, calls one of the many WriteXXXX methods that must be overridden by classes deriving from Formatter. The Formatter.WriteMember method doesn t discriminate on the string type. Instead, this method passes string objects to the WriteObjectRef method. To make for easier coding of the WriteObjectRef method (which we ll discuss shortly), we ve chosen to override the implementation of WriteMember and handle strings as a special case. We re also handling Type instances as a special case, which we ll explain shortly. For all other types and if the object is null, we delegate to the base implementation of WriteMember:

protected override void WriteMember(string name, object data) { if ( data == null ) { base.WriteMember(name, data); } else if ( data.GetType() == typeof(string) ) { WriteString(data.ToString(), name); } else if ( data.GetType().IsSubclassOf(typeof(System.Type))) { WriteType(data, name); } else { base.WriteMember(name, data); } }

The WriteString method writes a string instance to the stream. In general, you have two options for serializing an instance of a string type, or any type for that matter. One option is to serialize the instance inline with the rest of the object members. Another option is to serialize an object reference for the instance and defer serializing the object instance until later. We ve chosen to serialize string instances inline with the rest of the object members. We implement the MyFormatter.WriteString method as follows:

private void WriteString(string val, string name) { // String types: // Because the Formatter class ignores the string type and // we've overridden the WriteMember method to handle strings // as a special case, calling WriteMember on a string type // ends up here. // We can treat it two ways: // (1) Write the string directly, or // (2) Write an object reference and schedule the // string object for later serialization. // // For our purposes, we'll just inline the string. if ( name != "" ) { WriteField(FieldNames.MEMBER_NAME, name); } // Write the member type. WriteField( FieldNames.MEMBER_TYPE, typeof(string).AssemblyQualifiedName); // Write the member value. WriteField(FieldNames.MEMBER_VALUE, val); }

The MyFormatter.WriteMember implementation also handles Type instances as a special case. The implementation for the WriteType method follows:

private void WriteType(object data, string name) { // Member name if ( name != "" ) { WriteField(FieldNames.MEMBER_NAME, name); } Type t = (System.Type)data; if ( t.FullName != "System.RuntimeType" ) { // Instead of serializing the type itself, just // set up to serialize full type name as a string and // flag it so that it's interpreted as a type rather // than a string. data = t.AssemblyQualifiedName; } else { throw new SerializationException("Unexpected type"); } // Member type WriteField(FieldNames.MEMBER_TYPE, typeof(string).AssemblyQualifiedName); // The value should be interpreted as a // type during deserialization. WriteField(FieldNames.MEMBER_VALUE_TYPE, data); }

The common language runtime treats Type instances as instances of System.RuntimeType. It just so happens that the RuntimeType implements the ISerializable interface but doesn t implement the special constructor needed for deserialization. To get around this problem, we write the assembly qualified type name of the type that the Type instance represents and tag the value by using the FieldNames.MEMBER_VALUE_TYPE field name. For example, if the Type instance is typeof(SomeClass), we write the assembly qualified name for the SomeClass type rather than serialize the RuntimeType instance. During deserialization, we ll create a Type instance from the assembly qualified name by using the Type.GetType method.

The remaining methods needed to complete the serialization implementation are virtual members of the Formatter class that the WriteMember method calls. These methods are WriteArray, WriteObjectRef, WriteValueType, and the type-safe WriteXXXX methods for primitive types. The WriteArray implementation follows:

protected override void WriteArray( object obj, string name, Type memberType) { if ( name != "" ) { // // Member name is not "", which indicates that this object // is a member of another object. Instead of serializing the // array inline with the parent object, we'll just // serialize a reference to it and schedule it for later // serialization. WriteObjectRef(obj,name, memberType); } else { // // Go ahead and serialize the array directly. // To create an array, we need the array type, lengths, // and lower bounds of each dimension. System.Array a = (Array)obj; // For now, this formatter supports one-dimensional arrays // only. if ( a.Rank != 1 ) { throw new NotSupportedException(  "This formatter supports only 1-dimensional arrays"); } WriteField(FieldNames.ARRAY_RANK, a.Rank); for(int i = 0; i < a.Rank; ++i) { WriteField(FieldNames.ARRAY_LENGTH, a.GetLength(i)); WriteField(FieldNames.ARRAY_LOWERBOUND, a.GetLowerBound(i)); } // Write the array elements. for(int i = 0; i < a.Length; ++i) { object el = a.GetValue(i); if ( el != null && el.GetType().IsArray ) { // The array element itself is an array. // We'll just serialize a reference to it and // schedule it for later serialization. WriteObjectRef(el, "", memberType); } else { WriteMember("", el); } } } }

As shown in the previous listing, the WriteArray method calls the WriteObjectRef method to serialize an object reference to the array rather than serializing the array itself if the name parameter isn t empty. Obviously, when serializing an object, you need to serialize enough information to deserialize the object. For arrays, we write the rank of the array, lower bound, and length to the stream. We then iterate over each element of the array and write it to the stream. To prevent nested arrays, if an array element is itself an array, we write an object reference. To keep the example as simple as possible, the MyFormatter class supports serializing only arrays of one dimension.

The WriteObjectRef method needs to perform two functions. First, it should write an object reference to the stream. Second, it should schedule the object for later serialization by calling the Formatter.Schedule method. As with other member values, we write the member name, member type, and either a special indicator for null values or the object identifier returned from the Formatter.Schedule method:

// Write an object reference to the stream. protected override void WriteObjectRef( object obj, string name, Type memberType ) { // Member name if ( name != "" ) { WriteField(FieldNames.MEMBER_NAME, name); } // Member type WriteField(FieldNames.MEMBER_TYPE, memberType.AssemblyQualifiedName); // Member value if ( obj == null ) { // Null: // We'll use a special field indicator for null values. WriteField(FieldNames.MEMBER_VALUE_NULL, ""); } else { // Object: // We need to schedule this object for serialization. long id = Schedule(obj); // Write a reference to the object ID rather than // the complete object. WriteField(FieldNames.OBJECT_REFID, id); } }

For value types other than the primitive types, the WriteMember method calls the WriteValueType method. If specified, we write the member name, followed by the member type, and then the member value. Because the System.Void class represents the void type, we need to handle it as a special case in this method. You can t create an instance of System.Void directly. Therefore, we use the same technique as used in the WriteType method and simply write the assembly qualified name of the System.Void type and tag it so that it s handled correctly during deserialization:

protected override void WriteValueType( object obj, string name, Type memberType ) { // Write the member name if specified. if ( name != "" ) { WriteField(FieldNames.MEMBER_NAME, name); } // Write the member type. WriteField(FieldNames.MEMBER_TYPE, memberType.AssemblyQualifiedName); // Write the member value. // Special case for void types if ( memberType.FullName == "System.Void" ) { WriteField( FieldNames.MEMBER_VALUE_TYPE, memberType.AssemblyQualifiedName ); } else { WriteField(FieldNames.MEMBER_VALUE, obj); } }

The implementations of the remaining virtual protected WriteXXXX methods forward the call to the WriteValueType method. With the inclusion of this code, we have a fully functional IFormatter.Serialize method.

protected override void WriteBoolean(bool val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteByte(byte val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteChar(char val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteDateTime(System.DateTime val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteDecimal(decimal val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteDouble(double val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteInt16(short val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteInt32(int val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteInt64(long val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteSByte(sbyte val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteSingle(float val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteTimeSpan(System.TimeSpan val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteUInt16(ushort val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteUInt32(uint val, string name) { WriteValueType(val, name, val.GetType()); } protected override void WriteUInt64(ulong val, string name) { WriteValueType(val, name, val.GetType ()); }

Implementing the IFormatter.Deserialize Method

The function of the IFormatter.Deserialize method is to deserialize an object graph from a stream and return the root object of the object graph. The following code implements the IFormatter.Deserialize method for the MyFormatter class:

public override object Deserialize(System.IO.Stream serializationStream) { // // Create an object manager to help with deserialization. _om = new ObjectManager( _surrogateselector, _streamingcontext ); _reader = new StreamReader(serializationStream); // Read objects until end of stream. while( _reader.Peek() != -1 ) { ReadObject(); } // // Now we can do fixups and get the top object. _om.DoFixups(); // Return topmost object. return _om.GetObject(1); }

The MyFormatter.Deserialize method uses the ObjectManager class that we discussed earlier in the section to reconstruct the object graph. After creating a StreamReader instance around the stream, the code loops through the stream, reading the next object until the end of the stream is reached, which is indicated by a return value of 1 from the StreamReader.Peek method. After deserializing the object graph from the stream, we get the ObjectManager to perform fixups of the object graph by calling the DoFixup method and to return the root of the object graph. The following listing shows the implementation of the ReadObject method:

void ReadObject() { // Read object ID. long oid = FieldNames.ParseObjectID(_reader.ReadLine()); // Read object assembly. string s_oassembly = FieldNames.ParseObjectAssembly(_reader.ReadLine()); // Read object type. string s_otype = FieldNames.ParseObjectType(_reader.ReadLine()); // Read object members for this type. Type t = System.Type.GetType( String.Format( "{0},{1}", s_otype, s_oassembly ) ); if ( t.IsArray ) { ReadArray(oid, t); } else if ( t == typeof(string) ) { object o = FieldNames.ParseMemberValue(_reader.ReadLine()); _om.RegisterObject(o, oid); } else { SerializationInfo info; object o = ReadObjectMembers(oid, t, out info); if ( info == null ) { _om.RegisterObject(o, oid); } else { _om.RegisterObject(o,oid,info); } } }

The ReadObject method is basically the inverse of the WriteObject method. ReadObject reads the object identifier, assembly name, and object type from the stream. Recall that WriteObject handles instances of the string and array types differently than other types. That means that ReadObject needs to handle them differently as well. If the type is an array, it reads the array by calling the ReadArray method, which we ll discuss later in this section. If the object type is a string, we read the string s value from the stream by using the FieldNames.ParseMemberValue method, which we defined earlier. At this point, the entire object has been read, so we register the object with the ObjectManager, _om. For all other types, the ReadObject method reads the serialized object members from the stream by calling the ReadObjectMembers method, shown in the following listing:

private void ReadObjectMembers( long oid, Type t, out SerializationInfo info ) { info = null; // Try and find a surrogate for this type. ISerializationSurrogate surrogate = null; if ( _surrogateselector != null ) { ISurrogateSelector selector; surrogate = _surrogateselector.GetSurrogate( t, _streamingcontext, out selector ); } object o = FormatterServices.GetUninitializedObject( t ); // Read object members. if ( surrogate != null ) { // Yes, a surrogate is registered; this indicates that // the serialized members of a serialization info follow. ReadSerializationInfo(o, oid, out info); } else if ( this.IsMarkedSerializable(o) && (o is ISerializable) ) { // The object handles its own serialization. ReadSerializationInfo(o, oid, out info); } else if ( this.IsMarkedSerializable(o) ) { ReadSerializableMembers(o,oid); } else { // The type cannot be deserialized. throw new SerializationException(); } return o; }

The ReadObjectMembers method is the deserialization counterpart of the WriteObjectMembers method, and the two methods structures are similar. ReadObjectMembers first checks to see whether the formatter has a surrogate selector. If so, the method asks the surrogate selector whether it has a serialization surrogate for the type. If a surrogate exists or the type implements ISerializable, the method calls the ReadSerializationInfo method, which will allocate and initialize the output parameter, info, with the deserialized SerializationInfo members. If no surrogate exists and the type doesn t implement the ISerializable method but is attributed with the SerializableAttribute attribute, we call the ReadSerializableMembers method. If the type doesn t support serialization, it can t be deserialized, so we throw a SerializationException exception. The implementation of the ReadSerializationInfo method follows:

void ReadSerializationInfo( Object o, long oid, out SerializationInfo info ) { info = new SerializationInfo(o.GetType(), new FormatterConverter()); // Read the member count. long count = FieldNames.ParseMemberCount(_reader.ReadLine()); for( int i = 0; i < count; ++i) { ReadMember(oid, null, o, info); } }

The ReadSerializationInfo method is the counterpart to the WriteSerializationInfo. It reads the member count from the stream and then reads each member from the stream by calling the ReadMember method, which we ll discuss shortly.

Similar to ReadSerializationInfo, the ReadSerializableMembers method obtains an array of the MemberInfo instances for a type s serializable members and then reads each member from the stream by calling the ReadMember method:

void ReadSerializableMembers( Object o, long oid) { MemberInfo[] mi = FormatterServices.GetSerializableMembers(o.GetType()); // Read each member. for( int i = 0; i < mi.Length; ++i ) { ReadMember(oid, mi[i], o, null); } }

The following code listing shows the implementation for the ReadMember method that reads a member from the stream:

void ReadMember( long oid, MemberInfo mi, object o, SerializationInfo info ) { // Read member name. string sname = FieldNames.ParseMemberName(_reader.ReadLine()); // Read member type. string stype = FieldNames.ParseMemberType(_reader.ReadLine()); // Read member value. string svalue = _reader.ReadLine(); long roid = 0; object ovalue = ReadMemberValue(svalue, stype, ref roid); if ( roid != 0 ) { // Have we encountered the object yet? if ( ovalue == null ) { // If the object has a serialization info, // record a delayed fixup. if ( info != null ) { _om.RecordDelayedFixup(oid, sname, roid); } else { _om.RecordFixup(oid, mi, roid); } return; } } if ( info != null ) { info.AddValue(sname, ovalue); } else { FormatterServices.PopulateObjectMembers( o, new MemberInfo[]{mi}, new object[]{ovalue}); } }

The ReadMember method reads the member name, member type, and member value from the stream. To read the member s value from the stream, the ReadMember method calls the ReadMemberValue method, which we ll discuss momentarily. The return value of the ReadMemberValue method is the value for the member, and the last parameter, named roid, will be nonzero if the member references another object in the object graph. If the member s value references an object in the graph that hasn t yet been deserialized, we need to record a fixup for the member to the object instance by its object identifier. If the member s object has a SerializationInfo, we record a delayed fixup for the member name by using the RecordDelayedFixup method. Otherwise, we record a fixup by using the MemberInfo instance. If roid is 0, the member s value doesn t reference another object in the object graph and we can use the value to initialize the member of the object. If the object has a SerializationInfo, we add the member s value to the SerializationInfo instance. Otherwise, we use the FormatterServices.PopulateObjectMembers method to initialize the object s member with the deserialized value. The following listing shows the implementation for the ReadMemberValue method:

private object ReadMemberValue(string svalue, string stype, ref long rfoid) { if ( svalue.StartsWith( FieldNames.OBJECT_REFID ) ) { // This member references another object. rfoid = FieldNames.ParseObjectRefID(svalue); return _om.GetObject(rfoid); } else if ( svalue.StartsWith( FieldNames.MEMBER_VALUE_TYPE ) ) { // This member should be interpreted as a type. string s = FieldNames.ParseMemberValueType(svalue); return Type.GetType(s); } else if ( svalue.StartsWith( FieldNames.MEMBER_VALUE ) ) { // Value type; convert from string representation // to actual type. string s = FieldNames.ParseMemberValue(svalue); return Convert.ChangeType(s, Type.GetType(stype)); } else if ( svalue.StartsWith( FieldNames.MEMBER_VALUE_NULL ) ) { // Value is null. return null; } else { throw new SerializationException("Parse error."); } }

The ReadMemberValue method checks to see what kind of field tag starts the svalue parameter. For our formatter, four possibilities exist. The member value can be a reference to another object, in which case we parse the object identifier and return the object corresponding to the object identifier if the object has already been deserialized. Another possibility is that the member value should be interpreted as a Type. In that case, we parse the member value and create a Type instance by using the Type.GetType method. Or, the member value might just be the string representation of a primitive type or a value type, in which case we parse the member value and use the Convert.ChangeType method to convert the string to an instance of the serialized type. The last possibility is that the member value is null, and in that case, we return null.

The last two methods we need to define read an array object from the stream. The following listing defines the ReadArray method:

object ReadArray(long arrayId, Type t) { // Read the array rank. long rank = FieldNames.ParseArrayRank(_reader.ReadLine()); // Currently, we only support rank == 1. if ( rank != 1 ) { throw new System.NotSupportedException(  "This formatter supports only 1-dimensional arrays"); } long length = FieldNames.ParseArrayLength(_reader.ReadLine()); long lowerbound = FieldNames.ParseArrayLowerBound(_reader.ReadLine()); // Use Array.CreateInstance to create the array. Array oa = Array.CreateInstance(t.GetElementType(), (int)length); // Need to register the array in case we need to fixup elements. _om.RegisterObject(oa, arrayId); // Read array elements. for(int i=0; i < length; ++i) { ReadArrayElement(oa, arrayId, i, t.GetElementType()); } return oa; }

The ReadArray method reads the array rank, lower bound, and length from the stream. For the purposes of our example, we support one-dimensional arrays only. While developing this method, we tried using the FormatterServices.GetUninitializedObject method to create an instance of the array. This resulted in the throwing of an ExecutionEngine exception. We also tried simply creating a generic object array (object []), but that caused an exception to occur when casting the return value of the Deserialize method to an array of the appropriate type (for example, int []). The only way we could get this method to work was by calling the Array.CreateInstance method to create a type-safe array of the specified type and length. After creating the array instance, we registered it with the ObjectManager, _om. Following registration, we read each of the array elements by using the ReadArrayElement method defined in the following listing:

void ReadArrayElement(System.Array oa, long oid, int index, Type el_type) { // Read the type. string stype = FieldNames.ParseMemberType(_reader.ReadLine()); Type t = Type.GetType(stype); // Read the value. string svalue = _reader.ReadLine(); long roid = 0; object ovalue = ReadMemberValue(svalue, stype, ref roid); if ( roid != 0 ) { // Have we encountered the object yet? if ( ovalue == null ) { _om.RecordArrayElementFixup(oid, index, roid); return; } } oa.SetValue(ovalue,index) ; }

The ReadArrayElement method reads the type and value from the stream. As we did for the ReadMember method, after calling the ReadMemberValue method, we check the value of the roid variable. A nonzero value indicates that the array element references another object instance in the graph. If the object returned by ReadMemberValue is null, we haven t yet deserialized the object that the member references. In that case, we need to record a fixup by using the RecordArrayElementFixup method and return. Otherwise, the array element doesn t reference another object in the graph or the array element value is null. Either way, we set the array element s value to the object returned by the ReadMemberValue method.

At this point, we have a fully functional serialization formatter. Now that we ve developed a custom formatter, we can examine the procedure for plugging it into the .NET Remoting architecture.



Microsoft. NET Remoting
Microsoft .NET Remoting (Pro-Developer)
ISBN: 0735617783
EAN: 2147483647
Year: 2005
Pages: 46

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net