Section 3.1. Serialization


3.1. Serialization

The data contract is part of the contractual operations the service supports, just like the service contract is part of that contract. The data contract is published in the service metadata, which allows clients to convert the neutral, technology-agnostic representation to the client's native representation. Because objects and local references are CLR concepts, you cannot pass to and from a WCF service operation your CLR objects and references. Allowing you to do so would not just violate the core service-oriented principle discussed previously, but would also be impractical, since the object is comprised of both the 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.

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 finishes 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, WCF will deserialize the returned values into native CLR types and return them to the client.

3.1.1. .NET Serialization

WCF could make use of the ready-made support .NET offers for serialization. .NET automatically serializes and deserializes the objects using reflection. .NET captures the value of every field of an object and serializes it to memory, a file, or a network connection. For deserializing, .NET creates a new object of the matching type; reads its persisted field values; and sets the value of its fields, using reflection. Because reflection can access private fields, including base-class 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. A stream is a logical sequence of bytes, independent of a particular medium such as a file, memory, a communication port, or other resource.

3.1.1.1. 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, then after constructing a new object by deserializing it from the stream, you would end up with a defective object. Consequently, serialization has to be performed by consent of the class' developer.

To indicate to .NET that instances of your class are serializable, you 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] public class MyClass {...} 

When a class is serializable, .NET insists that all its member variables be serializable as well, and if it discovers a nonserializable member, it throws an exception. However, what if the class or a struct 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 nonserializable member is a reference type requiring some special initialization. The solution to this problem requires marking such a member as nonserializable and taking a custom step to initialize it during deserialization.

To allow a serializable type to contain a nonserializable type as a member variable, you need to mark the member with the NonSerialized field attribute; for example:

 public class MyOtherClass {..} [Serializable] public 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.

This allows you to preclude from serialization even normally serializable types such as the string:

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

3.1.1.2. The .NET formatters

.NET offers two formatters used for serializing and deserializing types. The BinaryFormatter serializes into a compact binary format, enabling fast serialization and deserialization because no parsing is required. The SoapFormatter uses a .NET-specific SOAP XML format, and it introduces composition overhead during serialization and parsing overhead during deserialization.

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,... {...} 

Regardless of the format used, 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 because it requires the other party to have the type assembly, and of course be using .NET in the first place. The use of the Stream is also an imposition because it requires the client and the service to somehow share the stream.

3.1.2. The WCF Formatters

Due to the deficiencies of the classic .NET formatters, WCF has to provide its own service-oriented formatter. The WCF 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

 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 only captures the state of the object according to the serialization or data contract schema. Note also that DataContractSerializer does not support IFormatter.

WCF automatically uses DataContractSerializer under the covers, and developers 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 the binary or SOAP formatters, you need to supply the constructor of DataContractSerializer 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.Seek(0,SeekOrigin.Begin);    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 media like 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( ) 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.Seek(0,SeekOrigin.Begin);    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, and 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.Seek(0,SeekOrigin.Begin);    MyClass obj2 = (MyClass)formatter.Deserialize(stream); } 

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

 MyClass obj1 = new MyClass( ); Stream stream = new MemoryStream( ); IFormatter formatter1 = new NetDataContractSerializer( ); formatter1.Serialize(stream,obj1); stream.Seek(0,SeekOrigin.Begin); DataContractSerializer formatter2 = new DataContractSerializer(typeof(MyClass)); MyClass obj2 = (MyClass)formatter2.ReadObject(stream); stream.Close( ); 

This ability 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.

3.1.3. 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 that 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 the string:

 [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 an implicit data contract for the primitive types because there is an industry standard for the schema of those types.

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

For example, 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 will be able to 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 as well:

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




Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

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