While using the Serializable attribute is
workable, it is not ideal for service-oriented interaction between clients
and services. Rather than denoting all members in a type as serializable
and therefore part of the data schema for that type, it would be
preferable to have an opt-in approach, where only members the contract
developer wants to explicitly include in the data contract are included.
The Serializable attribute forces the
data type to be serializable in order to be used as a parameter in a
contract operation, and it does not offer clean separation between the
ability to use the type as a WCF operation parameter (the “serviceness”
aspect of the type) and the ability to serialize it. The attribute offers
no support for aliasing type names or members, or for mapping a new type
to a predefined data contract. The attribute operates directly on member
fields and completely bypasses any logical properties used to access those
fields. It would be better to allow those properties to add their values
when accessing the fields. Finally, there is no direct support for
versioning, because the formatter supposedly captures all versioning
information. Consequently, it is difficult to deal with versioning over
time.Yet again, the WCF solution is to come up with new service-oriented
opt-in attributes. The first of these attributes is the DataContractAttribute, defined in the System.Runtime.Serialization namespace:
[AttributeUsage(AttributeTargets.Enum |
AttributeTargets.Struct|
AttributeTargets.Class,
Inherited = false,
AllowMultiple = false)]
public sealed class DataContractAttribute : Attribute
{
public string Name
{get;set;}
public string Namespace
{get;set;}
//More members
}
Applying the DataContract
attribute on a class or struct does not cause WCF to serialize any of its
members:
[DataContract]
struct Contact
{
//Will not be part of the data contract
public string FirstName;
public string LastName;
}
All the DataContract attribute
does is opt-in the type, indicating that the type can be marshaled by value. To serialize any of
its members, you must apply the DataMemberAttribute, defined as:
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property,
Inherited = false,AllowMultiple = false)]
public sealed class DataMemberAttribute : Attribute
{
public bool IsRequired
{get;set;}
public string Name
{get;set;}
public int Order
{get;set;}
//More members
}
You can apply the DataMember
attribute on the fields directly:
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
You can also apply the DataMember
attribute on properties (either explicit properties, where you provide the
property implementation, or automatic properties, where the compiler
generates the underlying member and access implementation):
[DataContract]
struct Contact
{
string m_FirstName;
[DataMember]
public string FirstName
{
get
{
return m_FirstName;
}
set
{
m_FirstName = value;
}
}
[DataMember]
public string LastName
{get;set;}
}
As with service contracts, the visibility of the data members and
the data contract itself is of no consequence to WCF. Thus, you can
include internal types with private data members in the data
contract:
[DataContract]
struct Contact
{
[DataMember]
string FirstName
{get;set;}
[DataMember]
string LastName
{get;set;}
}
Some of the code in this chapter applies the DataMember attribute
directly on public data members for brevity’s sake. In real code, you
should, of course, use properties instead of public members.
Note: Data contracts are case-sensitive both at the type and member
levels.
1. Importing a Data Contract
When a data contract is used in a contract operation, it
is published in the service metadata. When the client uses a tool such
as Visual Studio 2010 to import the definition of the data
contract, the client will end up with an equivalent definition, but not
necessarily an identical one. The difference is a function of the tool,
not the published metadata. With Visual Studio 2010, the imported
definition will maintain the original type designation of a class or a
structure as well as the original type namespace, but with SvcUtil, only the data contract will maintain the
namespace. Take, for example, the following service-side
definition:
namespace MyNamespace
{
[DataContract]
struct Contact
{...}
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
Contact[] GetContacts();
}
}
The imported definition will be:
namespace MyNamespace
{
[DataContract]
struct Contact
{...}
}
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
Contact[] GetContacts();
}
To override this default and provide an alternative namespace for
the data contract, you can assign a value to the Namespace property of
the DataContract attribute. The tools
treat the provided namespace differently. Given this service-side
definition:
namespace MyNamespace
{
[DataContract(Namespace = "MyOtherNamespace")]
struct Contact
{...}
}
Visual Studio 2010 imports it exactly as defined, while
SvcUtil imports it as published:
namespace MyOtherNamespace
{
[DataContract]
struct Contact
{...}
}
When using Visual Studio 2010, the imported definition will always
have properties decorated with the DataMember attribute,
even if the original type on the service side did not define any
properties. For example, for this service-side definition:
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
The imported client-side definition will
be:
[DataContract]
public partial struct Contact
{
string FirstNameField;
string LastNameField;
[DataMember]
public string FirstName
{
get
{
return FirstNameField;
}
set
{
FirstNameField = value;
}
}
[DataMember]
public string LastName
{
get
{
return LastNameField;
}
set
{
LastNameField = value;
}
}
}
The client can, of course, manually rework any imported definition
to be just like a service-side definition.
Note: Even if the DataMember
attribute on the service side is applied on a private field or
property, as shown here:the imported definition will have a public property
instead.[DataContract]
struct Contact
{
[DataMember]
string FirstName
{get;set;}
[DataMember]
string LastName;
}
When the DataMember
attribute is applied on a property (on either the service or the client
side), that property must have get
and set accessors. Without them, you
will get an InvalidDataContractException at call time. The
reason is that when the property itself is the data member, WCF uses the
property during serialization and deserialization, letting you apply any
custom logic in the property.
Warning: Do not apply the DataMember
attribute on both a property and its underlying field—this will result
in duplication of the members on the importing side.
It is important to realize that the method just described for
utilizing the DataMember attribute
applies to both the service and the client side. When the client uses
the DataMember attribute (and its
related attributes, described elsewhere in this chapter), it affects the
data contract it is using to either serialize and send parameters to the
service or deserialize and use the values returned from the service. It
is quite possible for the two parties to use equivalent yet not
identical data contracts, and, as you will see later, even to use
nonequivalent data contracts. The client controls and configures its
data contract independently of the service.
2. Data Contracts and the Serializable Attribute
The service can still use a type that is only marked with
the Serializable attribute:
[Serializable]
struct Contact
{
string m_FirstName;
public string LastName;
}
When importing the metadata of such a type, the imported
definition will use the DataContract
attribute. In addition, since the Serializable attribute affects only fields, it
will be as if every serializable member (whether public or private) is a
data member, resulting in a set of wrapping properties named exactly
like the original fields:
[DataContract]
public partial struct Contact
{
string LastNameField;
string m_FirstNameField;
[DataMember(...)]
public string LastName
{
... //Accesses LastNameField
}
[DataMember(...)]
public string m_FirstName
{
... //Accesses m_FirstNameField
}
}
The client can also use the Serializable attribute on its data contract to
have it marshaled in much the same way.
Note: A type marked only with the DataContract attribute cannot be serialized
using the legacy formatters. If you want to serialize such a type, you
must apply both the DataContract
attribute and the Serializable
attribute on it. In the resulting data contract for the type, the
effect will be the same as if only the DataContract attribute had been applied, and
you will still need to use the DataMember attribute on the members you want
to serialize.
.NET offers yet another serialization mechanism: raw XML
serialization, using a dedicated set of attributes. When you’re
dealing with a data type that requires explicit control over XML
serialization, you can use the XmlSerializerFormatAttribute on individual
operations in the contract definition to instruct WCF to use XML
serialization at runtime. If all the operations in the contract
require this form of serialization, you can use the /serializer:XmlSerializer switch of
SvcUtil
to instruct it to automatically apply the XmlSerializerFormat attribute on all
operations in all imported contracts. Use this switch with caution,
though, because it will affect all data contracts, including those
that do not require explicit control over XML serialization. |
3. Inferred Data Contracts
WCF provides support for inferred data
contracts. If the marshaled type is a public type and it is
not decorated with the DataContract
attribute, WCF will automatically infer such an attribute and apply the
DataMember attribute to
all public members (fields or properties) of the type.
For example, given this service contract definition:
public struct Contact
{
public string FirstName
{get;set;}
public string LastName;
internal string PhoneNumber;
string Address;
}
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
...
}
WCF will infer a data contract, as if the service contract
developer had defined it as:
[DataContract]
public class Contact
{
[DataMember]
public string FirstName
{get;set;}
[DataMember]
public string LastName;
}
The inferred data contract will be published in the service
metadata.
If the type already contains DataMember attributes (but not a DataContract attribute), these data member
contracts will be ignored as if they were not present. If the type does
contain a DataContract attribute, no
data contract is inferred. Likewise, if the type is internal, no data
contract is inferred. Furthermore, all subclasses of a class that
utilizes an inferred data contract must themselves be inferable; that
is, they must be public classes and have no DataContract attribute.
Note: Microsoft calls inferred data contracts POCOs, or “plain old CLR objects.”
In my opinion, relying on inferred data contracts is a sloppy hack
that goes against the grain of most everything else in WCF. Much as WCF
does not infer a service contract from a mere interface definition or
enable transactions or reliability by default, it should not infer a
data contract. Service orientation (with the exception of security) is
heavily biased toward opting out by default, as it should be, to
maximize encapsulation and decoupling. Do use the DataContract attribute, and be explicit about
your data contracts. This will enable you to tap into data contract
features such as versioning. The rest of this book does not use or rely
on inferred data contracts.
4. Composite Data Contracts
When you define a data contract, you can apply the DataMember attribute on members that are
themselves data contracts, as shown in Example 1.
Example 1. A composite data contract
[DataContract] class Address { [DataMember] public string Street;
[DataMember] public string City;
[DataMember] public string State;
[DataMember] public string Zip; } [DataContract] struct Contact { [DataMember] public string FirstName;
[DataMember] public string LastName;
[DataMember] public Address Address; }
|
Being able to aggregate other data contracts in this way
illustrates the fact that data contracts are actually recursive in
nature. When you serialize a composite data contract, the DataContractSerializer
will chase all applicable references in the object graph and capture
their state as well. When you publish a composite data contract, all its
comprising data contracts will be published as well. For example, using
the same definitions as those in Example 3-3, the metadata for this service
contract:
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
Contact[] GetContacts();
}
will include the definition of the Address structure as
well.
5. Data Contract Events
.NET provides support for serialization events for
serializable types, and WCF provides the same support for data
contracts. WCF calls designated methods on your data contract when serialization and
deserialization take place. Four serialization and deserialization
events are defined. The serializing event is raised just
before serialization takes place and the serialized
event is raised just after serialization. Similarly, the
deserializing event is raised just
before deserialization and the deserialized event
is raised after deserialization. You designate methods as serialization
event handlers using method attributes, as shown in Example 2.
Example 2. Applying the serialization event attributes
[DataContract] class MyDataContract { [OnSerializing] void OnSerializing(StreamingContext context) {...}
[OnSerialized] void OnSerialized(StreamingContext context) {...}
[OnDeserializing] void OnDeserializing(StreamingContext context) {...}
[OnDeserialized] void OnDeserialized(StreamingContext context) {...} //Data members }
|
Each serialization event-handling method must have the following
signature:
void <Method Name>(StreamingContext context);
If the serialization event attributes (defined in the System.Runtime.Serialization namespace) are
applied on methods with incompatible signatures, WCF will throw an
exception.
The StreamingContext
structure informs the type of why it is being serialized, but it can be
ignored for WCF data contracts.
As their names imply, the OnSerializing attribute
designates a method to handle the serializing event and the OnSerialized attribute
designates a method to handle the serialized event. Similarly, the
OnDeserializing
attribute designates a method to handle the deserializing event and the
OnDeserialized
attribute designates a method to handle the deserialized event.
Figure 1 is an
activity diagram depicting the order in which events are raised during
serialization.