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.
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.
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.
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.
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 Serializable
Attribute
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 {...}
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;
}
.NET offers two
formatters for serializing and deserializing types. The Binary
Formatter
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.
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 DataContract
Serializer
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 Data
Contract
Serializer
, 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 Net
DataContractSerializer();
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 Data
Contract
Serializer
:
MyClass obj1 = new MyClass();
IFormatter formatter1 = new Net
DataContractSerializer();
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.
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;
}
18.223.33.157