Serialization

The data contract is part of the contractual obligation the service supports, just like the service operations are part of that contract. The data contract is published in the service’s metadata, allowing clients to convert the neutral, technology-agnostic representation of the data types to their native representations. Because objects and local references are CLR concepts, you cannot pass CLR objects and references to and from a WCF service operation. Allowing you to do so not only would violate the core service-oriented principle discussed previously, but also would be impractical, since an object is comprised of both its state and the code manipulating it. There is no way of sending the code or the logic as part of a C# or Visual Basic method invocation, let alone marshaling it to another platform and technology. In fact, when passing an object (or a value type) as an operation parameter, all you really need to send is the state of that object, and you let the receiving side convert it back to its own native representation. Such an approach for passing state around is called marshaling by value. The easiest way to perform marshaling by value is to rely on the built-in support most platforms (.NET included) offer for serialization. The approach is simple enough, as shown in Figure 3-1.

Serialization and deserialization during an operation call

Figure 3-1. Serialization and deserialization during an operation call

On the client side, WCF will serialize the in-parameters from the CLR native representation to an XML infoset and bundle them in the outgoing message to the client. Once the message is received on the service side, WCF will deserialize it and convert the neutral XML infoset to the corresponding CLR representation before dispatching the call to the service. The service will then process the native CLR parameters. Once the service has finished executing the operation, WCF will serialize the out-parameters and the returned values into a neutral XML infoset, package them in the returned message, and post the returned message to the client. Finally, on the client side, WCF will deserialize the returned values into native CLR types and return them to the client.

Note

The double dose of serialization and deserialization in every call is the real bottleneck of WCF, performance-wise. The cost of running a message through the interceptors on the client and service sides is minuscule compared with the overhead of serialization.

.NET Serialization

WCF can make use of .NET’s ready-made support for serialization. .NET automatically serializes and deserializes objects using reflection. .NET captures the value of each of the object’s fields and serializes it to memory, to a file, or to a network connection. For deserializing, .NET creates a new object of the matching type, reads its persisted field values, and sets the values of its fields using reflection. Because reflection can access private fields, .NET takes a complete snapshot of the state of an object during serialization and perfectly reconstructs that state during deserialization. .NET serializes the object state into a stream, which is a logical sequence of bytes, independent of a particular medium such as a file, memory, a communication port, or any other resource.

The Serializable attribute

By default, user-defined types (classes and structs) are not serializable. The reason is that .NET has no way of knowing whether a reflection-based dump of the object state to a stream makes sense. Perhaps the object members have some transient value or state (such as an open database connection or communication port). If .NET simply serialized the state of such an object after constructing a new object by deserializing it from the stream, you could end up with a defective object. Consequently, serialization has to be performed by consent of the class’s developer.

To indicate to .NET that instances of your class are serializable, add the SerializableAttribute to your class or struct definition:

[AttributeUsage(AttributeTargets.Delegate|
                AttributeTargets.Enum    |
                AttributeTargets.Struct  |
                AttributeTargets.Class,
                Inherited = false)]
public sealed class SerializableAttribute : Attribute
{}

For example:

[Serializable]
class MyClass
{...}

The NonSerialized attribute

When a class is serializable, .NET insists that all its member variables be serializable as well, and if it discovers a non-serializable member, it throws an exception. However, what if the class or a struct that you want to serialize has a member that cannot be serialized? That type will not have the Serializable attribute and will preclude the containing type from being serialized. Commonly, that non-serializable member is a reference type requiring some special initialization. The solution to this problem requires marking such a member as non-serializable and taking a custom step to initialize it during deserialization.

To allow a serializable type to contain a non-serializable type as a member variable, you need to mark the member with the NonSerialized field attribute. For example:

class MyOtherClass
{...}

[Serializable]
class MyClass
{
   [NonSerialized]
   MyOtherClass m_OtherClass;
   /* Methods and properties */
}

When .NET serializes a member variable, it first reflects it to see whether it has the NonSerialized attribute. If so, .NET ignores that variable and simply skips over it.

You can even use this technique to exclude from serialization normally serializable types, such as string:

[Serializable]
class MyClass
{
   [NonSerialized]
   string m_Name;
}

The .NET formatters

.NET offers two formatters for serializing and deserializing types. The BinaryFormatter serializes into a compact binary format, enabling fast serialization and deserialization. The SoapFormatter uses a .NET-specific SOAP XML format.

Both formatters support the IFormatter interface, defined as:

public interface IFormatter
{
   object Deserialize(Stream serializationStream);
   void Serialize(Stream serializationStream,object graph);
   // More members
}

public sealed class BinaryFormatter : IFormatter,...
{...}
public sealed class SoapFormatter : IFormatter,...
{...}

In addition to the state of the object, both formatters persist the type’s assembly and versioning information to the stream so that they can deserialize it back to the correct type. This renders them inadequate for service-oriented interaction, however, because it requires the other party not only to have the type assembly, but also to be using .NET. The use of the Stream is also an imposition, because it requires the client and the service to somehow share the stream.

The WCF Formatters

Due to the deficiencies of the classic .NET formatters, WCF has to provide its own service-oriented formatter. The formatter, DataContractSerializer, is capable of sharing just the data contract, not the underlying type information. DataContractSerializer is defined in the System.Runtime.Serialization namespace and is partially listed in Example 3-1.

Example 3-1. The DataContractSerializer formatter

public abstract class XmlObjectSerializer
{
   public virtual object ReadObject(Stream stream);
   public virtual object ReadObject(XmlReader reader);
   public virtual void WriteObject(XmlWriter writer,object graph);
   public void WriteObject(Stream stream,object graph);
   //More members
}
public sealed class DataContractSerializer : XmlObjectSerializer
{
   public DataContractSerializer(Type type);
   //More members
}

DataContractSerializer captures only the state of the object according to the serialization or data contract schema. Note that DataContractSerializer does not support IFormatter.

WCF uses DataContractSerializer automatically under the covers, and developers should never need to interact with it directly. However, you can use DataContractSerializer to serialize types to and from a .NET stream, similar to using the legacy formatters. Unlike when using the binary or SOAP formatters, however, you need to supply the DataContractSerializer constructor with the type to operate on, because no type information will be present in the stream:

MyClass obj1 = new MyClass();
DataContractSerializer formatter = new DataContractSerializer(typeof(MyClass));

using(Stream stream = new MemoryStream())
{
   formatter.WriteObject(stream,obj1);
   stream.Position = 0;
   MyClass obj2 = (MyClass)formatter.ReadObject(stream);
}

While you can use DataContractSerializer with .NET streams, you can also use it in conjunction with XML readers and writers when the only form of input is the raw XML itself, as opposed to some medium such as a file or memory.

Note the use of the amorphous object in the definition of DataContractSerializer in Example 3-1. This means that there will be no compile-time-type safety, because the constructor can accept one type, the WriteObject() method can accept a second type, and the ReadObject() method can cast to yet a third type.

To compensate for that, you can define your own generic wrapper around DataContractSerializer, as shown in Example 3-2.

Example 3-2. The generic DataContractSerializer<T>

public class DataContractSerializer<T> : XmlObjectSerializer
{
   DataContractSerializer m_DataContractSerializer;

   public DataContractSerializer()
   {
      m_DataContractSerializer = new DataContractSerializer(typeof(T));
   }
   public new T ReadObject(Stream stream)
   {
      return (T)m_DataContractSerializer.ReadObject(stream);
   }
   public new T ReadObject(XmlReader reader)
   {
      return (T)m_DataContractSerializer.ReadObject(reader);
   }
   public void WriteObject(Stream stream,T graph)
   {
      m_DataContractSerializer.WriteObject(stream,graph);
   }
   public void WriteObject(XmlWriter writer,T graph)
   {
      m_DataContractSerializer.WriteObject(writer,graph);
   }
   //More members
}

The generic class DataContractSerializer<T> is much safer to use than the object-based DataContractSerializer:

MyClass obj1 = new MyClass();
DataContractSerializer<MyClass> formatter = new DataContractSerializer<MyClass>();

using(Stream stream = new MemoryStream())
{
   formatter.WriteObject(stream,obj1);
   stream.Position = 0;
   MyClass obj2 = formatter.ReadObject(stream);
}

WCF also offers the NetDataContractSerializer formatter, which is polymorphic with IFormatter:

public sealed class NetDataContractSerializer : IFormatter,...
{...}

As its name implies, similar to the legacy .NET formatters, the NetDataContractSerializer formatter captures the type information in addition to the state of the object. It is used just like the legacy formatters:

MyClass obj1 = new MyClass();
IFormatter formatter = new NetDataContractSerializer();

using(Stream stream = new MemoryStream())
{
   formatter.Serialize(stream,obj1);
   stream.Position = 0;
   MyClass obj2 = (MyClass)formatter.Deserialize(stream);
}

NetDataContractSerializer is designed to complement DataContractSerializer. You can serialize a type using NetDataContractSerializer and deserialize it using DataContractSerializer:

MyClass obj1 = new MyClass();
IFormatter formatter1 = new NetDataContractSerializer();

using(Stream stream = new MemoryStream())
{
   formatter1.Serialize(stream,obj1);

   stream.Position = 0;

   DataContractSerializer formatter2 = new DataContractSerializer(typeof(MyClass));
   MyClass obj2 = (MyClass)formatter2.ReadObject(stream);
}

This capability opens the way for versioning tolerance and for migrating legacy code that shares type information into a more service-oriented approach where only the data schema is maintained.

Data Contract via Serialization

When a service operation accepts or returns any type or parameter, WCF uses DataContractSerializer to serialize and deserialize that parameter. This means you can pass any serializable type as a parameter or returned value from a contract operation, as long as the other party has the definition of the data schema or the data contract. All the .NET built-in primitive types are serializable. For example, here are the definitions of the int and string types:

[Serializable]
public struct Int32 : ...
{...}

[Serializable]
public sealed class String : ...
{...}

This is the only reason why any of the service contracts shown in the previous chapters actually worked: WCF offers implicit data contracts for the primitive types because there is an industry standard for the schemas of those types.

To use a custom type as an operation parameter, there are two requirements: first, the type must be serializable, and second, both the client and the service need to have a local definition of that type that results in the same data schema.

Consider the IContactManager service contract used to manage a contacts list:

[Serializable]
struct Contact
{
   public string FirstName;
   public string LastName;
}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   void AddContact(Contact contact);

   [OperationContract]
   Contact[] GetContacts();
}

If the client uses an equivalent definition of the Contact structure, it can pass a contact to the service. An equivalent definition might be anything that results in the same data schema for serialization. For example, the client might use this definition instead:

[Serializable]
struct Contact
{
   public string FirstName;
   public string LastName;
   [NonSerialized]
   public string Address;
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.33.157