Microsoft .NET Remoting (Pro-Developer) - page 41

Summary

In this chapter, we looked at HttpChannel to gain an understanding of the structure of channels. We then used this knowledge create our own channel by using the file system as our transport. Regardless of whether you’ll ever need to create a custom channel, you should now have a greater understanding of what’s happening behind the scenes when you use HttpChannel or TcpChannel. We wrapped the discussion up with the creation of a custom sink. In Chapter 8, “Serialization Formatters,” we’ll extend this knowledge when we create a formatter sink.

Chapter 8

Serialization Formatters

In this chapter, we’ll develop a custom serialization formatter capable of plugging into the .NET Remoting infrastructure. Before we do that, we’ll examine the architecture that the .NET Framework uses to serialize object instances. We’ll also look at several of the classes that the .NET Framework defines that facilitate building a serialization formatter. Finally, we’ll develop a client formatter sink and a server formatter sink that we’ll use to serialize the .NET Remoting messages exchanged between remote objects.

Object Serialization

Serialization is the process of converting an object instance’s state into a sequence of bits that can then be written to a stream or some other storage medium. Deserialization is the process of converting a serialized bit stream to an instance of an object type. As we mentioned in Chapter 2, “Understanding the .NET Remoting Architecture,” the .NET Remoting infrastructure uses serialization to transfer instances of marshal-by-value object types across .NET Remoting boundaries. In the next few sections, we’ll discuss how serialization works in the .NET Framework so that you’ll have a better understanding of it when we develop a custom serialization formatter later in the chapter.

NOTE
For more information about object serialization, consult the MSDN documentation and the excellent series of articles by Jeffrey Richter in his .NET column of MSDN Magazine in the April 2002 and July 2002 issues.

Serializable Attribute

The .NET Framework provides an easy-to-use serialization mechanism for object implementers. To make a class, structure, delegate, or enumeration serializable, simply attribute it with the SerializableAttribute custom attribute, as shown in the following code snippet:

[Serializable]
class SomeClass
{
    public int  m_public = 5000;
    private int m_private = 5001;
}

Because we’ve attributed the SomeClass type with the SerializableAttribute, the common language runtime will automatically handle the serialization details for us. To prevent the serialization of a field member of a type attributed with the SerializableAttribute, you can attribute the field member with the NonSerializedAttribute.

To serialize object instances of the SomeClass type, we need a serialization formatter to do the serialization and a stream to hold the serialized bits. As we discussed in Chapter 2, the .NET Framework provides the SoapFormatter and BinaryFormatter classes for serializing object graphs to streams. The following code serializes an instance of the SomeClass type to a MemoryStream by using the SoapFormatter:

// Create a memory stream and serialize a 
// new instance of SomeClass to it using a formatter.
MemoryStream s = new MemoryStream();
SoapFormatter fm = new SoapFormatter();
fm.Serialize( s, new SomeClass() );

// Output the stream contents to the console.
StreamReader r = new StreamReader(s);
s.Position = 0;
Console.WriteLine( r.ReadToEnd() );
s.Position = 0;

// Deserialize the stream to an instance of SomeClass.
SomeClass sc = (SomeClass)fm.Deserialize(s);

Executing this code results in the SomeClass instance being serialized to the MemoryStream instance in a SOAP format. The following listing shows the contents of the memory stream after serialization of the SomeClass instance. (We’ve inserted spaces and new lines to help readability.)

<SOAP-ENV:Envelope
 xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
 xmlns:xsd="http://www.w3.org/2001/XMLSchema"
 xmlns:SOAP-ENC=http://schemas.xmlsoap.org/soap/encoding/
 xmlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/
 xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" 
 SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
        <a1:SomeClass  xmlns:
         a1="http://schemas.microsoft.com/clr/nsassem/RemoteObjects/
         RemoteObjects%2C%20Version%3D1.0.904.25890%2C%20
         Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
              <m_public>5000</m_public>
              <m_private>5001</m_private>
        </a1:SomeClass>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

After the <SOAP-ENV:Envelope> element, you can see that the SoapFormatter serialized the SomeClass instance as a child element of the <SOAP=ENV:Body> element. Notice that the <a1:SomeClass> element includes an id attribute equal to ref-1. As we’ll discuss later in the chapter, during serialization of an object graph, formatters assign each serialized object an object identifier that facilitates serialization and deserialization. Following the id attribute, the <a1:SomeClass> element includes an xml namespace attribute alias that indicates the complete assembly name, version, culture, and public key token information for the assembly that contains the SomeClass type. Each of the child elements of the <a1:SomeClass> element corresponds to a class member and contains the member’s value. The m_public member’s value is 5000, while the m_private member’s value is 5001.

Customizing Object Serialization

Although using SerializableAttribute is easy, it might not always satisfy your serialization requirements. The .NET Framework allows a type attributed with the SerializableAttribute custom attribute to handle its own serialization by implementing the ISerializable interface. This interface defines one method, GetObjectData:

void GetObjectData(
    SerializationInfo info,
    StreamingContext context);

During serialization, when the formatter encounters an instance of a type that implements the ISerializable interface, the formatter calls the GetObjectData method on the object instance, passing it a reference to a SerializationInfo instance and a reference to a StreamingContext instance. The SerializationInfo class is basically a specialized dictionary class that holds key-value pairs that the formatter will serialize to the stream. Table 8-1 lists some of the more significant public members of the SerializationInfo class.

Table 8-1. Significant Public Members of System.Runtime. Serialization.SerializationInfo

Member

Member Type

Description

AssemblyName

Read-write property

The full name of the assembly containing the type being serialized. Includes version information, culture, and public key token. You can modify this in GetObjectData to affect the assembly used during deserialization.

FullTypeName

Read-write property

Equivalent to Type.FullName of the type being serialized. You can modify this in GetObjectData to cause the type to be deserialized as a different type.

MemberCount

Read-only property

Indicates the number of members that have been added to the SerializationInfo instance.

AddValue

Method

Adds a named value to the SerializationInfo instance. This method has various overloads based on the type of the object being added.

GetValue

Method

Gets a named value from the SerializationInfo instance based on the type of the object being retrieved.

The object implementing GetObjectData uses the SerializationInfo instance referenced by the first parameter, info, to serialize any information it requires for later deserialization. The GetObjectData method’s second parameter, context, references an instance of StreamingContext and corresponds to the object referenced by the formatter’s Context property. The StreamingContext instance exposes two properties, Context and State, that convey additional information that can affect the serialization operation. Both properties are read-only and specified as parameters to the StreamingContext constructor. The State property can be any bitwise combination of the StreamingContextStates enumeration type members: All, Clone, CrossAppDomain, CrossMachine, CrossProcess, File, Other, Persistence, and Remoting. The Context property can reference any object and conveys user-defined data to the serialization and deserialization operation.

The following code defines a class that implements the ISerializable interface:

[Serializable]
public class SomeClass2 : ISerializable
{
    public int m_public = 5000;
    private int m_private = 5001;

    public void GetObjectData( SerializationInfo info,
                               StreamingContext context)
    {
        //
        // Add a datetime value to the serialization info.
        info.AddValue( "TimeStamp", DateTime.Now );

        //
        // Serialize object members.
        info.AddValue( "m1", m_public );
        info.AddValue( "m2", m_public );
    }

    // Special deserialization ctor
    protected SomeClass2( SerializationInfo info,
                          StreamingContext context)
    {
        // Retrieve object members from
        // the SerializationInfo instance.
        m_public = info.GetInt32("m1");
        m_private = info.GetInt32("m2");

        // Retrieve time stamp.
        DateTime ts = info.GetDateTime("TimeStamp");
    }
}

The SomeClass2 type implements the ISerializable.GetObjectData method. Using the info parameter, SomeClass2 adds a new value named TimeStamp that contains the current system date and time. Notice that in addition to the implementation of GetObjectData, the SomeClass2 type defines a special constructor that takes the same parameters as GetObjectData. The special constructor is an implicit requirement of implementing the ISerializable interface. If you fail to provide this form of the constructor, the runtime will raise an exception when deserializing an instance of the type. The following listing shows what a SOAP-formatted serialized instance of SomeClass2 looks like:

<SOAP-ENV:Envelope
 xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
 xmlns:xsd=http://www.w3.org/2001/XMLSchema
 xmlns:SOAP-ENC=http://schemas.xmlsoap.org/soap/encoding/
 xmlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/
 xmlns:clr=http://schemas.microsoft.com/soap/encoding/clr/1.0
 SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
        <a1:SomeClass2  xmlns:
         a1="http://schemas.microsoft.com/clr/nsassem/RemoteObjects/
         RemoteObjects%2C%20Version%3D1.0.904.32400%2C%20
         Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
            <TimeStamp xsi:type="xsd:dateTime">
                2002-06-23T19:00:22.7003264-04:00
            </TimeStamp>
            <m1>5000</m1>
            <m2>5001</m2>
        </a1:SomeClass2>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Notice that the <a1:SomeClass2> tag includes three child elements corresponding to the three values added to the SerializationInfo instance in GetObjectData. The name of each child element corresponds to the name specified for the SerializationInfo.AddData method. These names must be unique within the SerializationInfo instance.

Object Graph Serialization

In the earlier examples, the integer members appeared as child elements of the parent element. This is because the SoapFormatter serializes primitive types inline with the rest of the object. Let’s look at an example in which the object being serialized has a member referencing another object instance. In this case, the formatter serializes a reference identifier rather than serializing the referenced object inline. The formatter will serialize the referenced object at a later position in the stream.

Figure 8-1 shows the object graph resulting from instantiation of a SomeClass3 class, defined in the following code:

[Serializable]
public class SomeClass3 
{
    public SomeClass2 m_sc3 = new SomeClass2();
    public int m_n = 2112;
}

figure 8-1 an object graph

Figure 8-1. An object graph

Here’s a SOAP-formatted serialized object instance of the SomeClass3 type:

<SOAP-ENV:Envelope
 xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
 xmlns:xsd=http://www.w3.org/2001/XMLSchema
 xmlns:SOAP-ENC=http://schemas.xmlsoap.org/soap/encoding/
 xmlns:SOAP-ENV=http://schemas.xmlsoap.org/soap/envelope/
 xmlns:clr=http://schemas.microsoft.com/soap/encoding/clr/1.0
 SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
        <a1:SomeClass3  xmlns:
         a1="http://schemas.microsoft.com/clr/nsassem/
         BasicSerialization/BasicSerialization%2C%20
         Version%3D1.0.905.37158%2C%20Culture%3Dneutral%2C%20
         PublicKeyToken%3Dnull">
            <m_sc2 href="#ref-3"/>
            <m_n>2112</m_n>
        </a1:SomeClass3>
        <a1:SomeClass2  xmlns:
         a1="http://schemas.microsoft.com/clr/nsassem/
         BasicSerialization/BasicSerialization%2C%20
         Version%3D1.0.905.37158%2C%20Culture%3Dneutral%2C%20
         PublicKeyToken%3Dnull">
            <TimeStamp xsi:type="xsd:dateTime">
                2002-06-24T21:38:42.8411136-04:00
            </TimeStamp>
            <m1>5000</m1>
            <m2>5001</m2>
        </a1:SomeClass2>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Notice in this listing that the <SOAP-ENV:Body> element contains two child elements, each corresponding to a serialized object instance. The SomeClass3 instance is the root of the object graph and appears as the first child element of the <SOAP-ENV:Body> element. The <m_sc2> child element of the <a1:SomeClass3> element contains an href attribute that references the element with id equal to ref-3, which is the identifier of the SomeClass2 serialized instance in the <a1:SomeClass2> element.

Object Graph Deserialization

As the formatter deserializes an object graph, it will begin allocating and initializing object instances within the object graph as it reads the serialized data from the stream. During deserialization, the formatter keeps track of each object and the object’s identifier. Each object member in the serialized object graph can reference one of the following:

  • An uninitialized object that hasn’t yet been deserialized

  • A partially initialized object that has been deserialized

  • A fully initialized object that has been deserialized

The formatter might encounter an object member that references another object that the formatter hasn’t yet deserialized, such as the <m_sc2> child element of the <a1:SomeClass3> element shown in the preceding example. In that case, the formatter updates an internal structure that associates the object member with the referenced object’s identifier. Later, when the formatter deserializes the referenced object from the stream, it will initialize any object members referencing the object. For object members that reference an object that the formatter has deserialized, the formatter initializes the member with that object instance.

Knowing When Deserialization Has Completed

Whether you use the SerializableAttribute to allow the common language runtime to handle serialization details or you customize serialization by implementing the ISerializable interface, you might want to be notified when the formatter has finished deserializing the entire object graph. If so, you can implement the IDeserializationCallback interface. This interface has one method: OnDeserializationCallback. After a formatter has deserialized an entire object graph, it calls this method on any objects within the object graph implementing the IDeserializationCallback interface. When the formatter calls OnDeserializationCallback on an object, the object can be sure that all objects referenced by its members have been fully initialized. However, the object can’t be sure that other objects that its members reference and that implement the IDeserializationCallback interface have had their OnDeserializationCallback methods called.

Serialization Surrogates and Surrogate Selectors

Sometimes you have a type that you want to serialize, but the type doesn’t support serialization. Or maybe you need to augment the serialized information for a type with additional information. To provide extra flexibility in the serialization architecture, the .NET Framework makes use of surrogates and surrogate selectors. A surrogate is a class that can take over the serialization requirements for instances of other types. A surrogate selector is basically a collection of surrogates that, when asked, returns a suitable surrogate for a given type.

Surrogates

A surrogate implements the ISerializationSurrogate interface. This interface defines two methods: GetObjectData and SetObjectData. The signature of the ISerializationSurrogate.GetObjectData method is almost identical to that of ISerializable.GetObjectData. However, the ISerializationSurrogate.GetObjectData method takes one additional parameter of type object, which is the object to be serialized. The surrogate’s implementation of GetObjectData can add members to the SerializationInfo instance prior to serializing the object members, or it can completely replace the serialization functionality of the given object.

The signature of SetObjectData is similar to that of ISerializationSurrogate.GetObjectData except that SetObjectData returns a populated object instance and takes an additional parameter of type ISurrogateSelector, which we’ll discuss shortly. The surrogate’s implementation of SetObjectData can read members from the SerializationInfo instance that were added during serialization of the object instance in the surrogate’s GetObjectData implementation.

Surrogate Selectors

As we’ll discuss in the “Serialization Formatters” section, one of the properties that a formatter exposes is the SurrogateSelector property. You can set the SurrogateSelector property to any object that implements the ISurrogateSelector interface. Formatters use the surrogate selector to determine whether the type currently being serialized or deserialized has a surrogate. If the type does, the formatter uses the surrogate to handle serialization and deserialization of instances of that type. We’ll discuss the details of the serialization and deserialization process later in this chapter.

The .NET Framework defines the System.Runtime.Serialization.SurrogateSelector class, which implements the ISurrogateSelector interface. The SurrogateSelector class also defines the Add method that allows you to add a surrogate to its internal collection and the Remove method that allows you to remove a surrogate from this collection.

The TimeStamper Surrogate

The following example will demonstrate the use of serialization surrogates and surrogate selectors. We’ll implement a surrogate that adds a time-stamp member to the SerializationInfo instance for an object and then serializes the object instance. The following code defines the TimeStamperSurrogate class:

class TimeStamperSurrogate : ISerializationSurrogate
{
    public void GetObjectData ( object obj, 
                                SerializationInfo info, 
                                StreamingContext context )
    {
        //
        // Add a datetime value to the serialization info.
        info.AddValue( "TimeStamperSurrogate.SerializedDateTime", 
                       DateTime.Now );

        //
        // Serialize the object.
        if ( obj is ISerializable )
        {
            ((ISerializable)obj).GetObjectData(info, context);
        }
        else
        {
            MemberInfo[]  mi  = 
             FormatterServices.GetSerializableMembers(obj.GetType());

            object[] od  = FormatterServices.GetObjectData( obj,
                                                            mi );

            for( int i = 0; i < mi.Length; ++i)
            {
                info.AddValue( mi[i].Name, od[i] );
            }
        }
    }

    public System.Object SetObjectData ( object obj, 
                                        SerializationInfo info, 
                                        StreamingContext context, 
                                        ISurrogateSelector selector )
    {

        // Get the values that this surrogate added in GetObjectData.
        DateTime dt = (DateTime)info.GetValue( 
                          "TimeStamperSurrogate.SerializedDateTime", 
                          typeof(DateTime) );

        if ( obj is ISerializable )
        {
            ObjectManager om = new ObjectManager(selector, context);
            om.RegisterObject(obj,1,info);
            om.DoFixups();
            obj = om.GetObject(1);
        }
        else
        {
            MemberInfo[]  mi  = 
             FormatterServices.GetSerializableMembers(obj.GetType());
 
            object[] od  = FormatterServices.GetObjectData( obj,
                                                            mi );

            int i = 0;
            SerializationInfoEnumerator ie = info.GetEnumerator();
            while(ie.MoveNext())
            {
                if ( mi[i].Name == ie.Name )
                {
                    od[i] = Convert.ChangeType( ie.Value, 
                                       ((FieldInfo)mi[i]).FieldType);
                    ++i;
                }
            }
        
            FormatterServices.PopulateObjectMembers( obj, mi, od );
        }
        return obj;
    }
}

The TimeStamperSurrogate.GetObjectData method adds a value with the current date and time to the SerializationInfo instance, info. The method then allows the object to add values to the SerializationInfo instance if this instance implements the ISerializable interface. Otherwise, the method uses the FormatterServices class to obtain the values for the object’s serializable members and adds them to the SerializationInfo instance. We’ll discuss the FormatterServices class in the “Serialization Formatters” section of the chapter.

The TimeStamperSurrogate.SetObjectData method reverses the process of the GetObjectData by first obtaining the time-stamp value from the SerializationInfo instance, info. If the object implements ISerializable, the SetObjectData makes use of the ObjectManager class, which we’ll also discuss in the “Serialization Formatters” section of the chapter. For now, it’s enough to know that these statements result in a call to the special constructor on the ISerializable object with the SerializationInfo instance, info, and the StreamingContext instance, context. If the object doesn’t implement the ISerializable class, SetObjectData uses the FormatterServices class to obtain information about the object’s serializable members and retrieves the member’s values from the SerializationInfo instance.

RemotingSurrogateSelector

In addition to the SurrogateSelector class, the .NET Framework defines another surrogate selector, RemotingSurrogateSelector, which is used during serialization of .NET Remoting related types. Table 8-2 lists the various surrogate classes that the .NET Remoting infrastructure uses.

Table 8-2. .NET Remoting Surrogates

Class

Description

RemotingSurrogate

Handles serialization of marshal-by-reference object instances

ObjRefSurrogate

Handles serialization of ObjRef instances by adding a value named fIsMarshalled to the SerializationInfo instance indicating that the serialized ObjRef instance was passed as a parameter rather than the marshal-by-ref object it represents

MessageSurrogate

Handles serialization of the MethodCall, MethodResponse, ConstructionCall, and ConstructionCall messages by enumerating over the IMessage.Properties collection and adding each property to the SerializationInfo as appropriate

SoapMessageSurrogate

Handles special serialization requirements of SOAP messages