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 any 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;} }
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 is willing to 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;} }
You can apply the DataMember
attribute on the fields
directly:
[DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; }
Or you can apply it 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 m_FirstName; [DataMember] string m_LastName; }
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.
Data contracts are case-sensitive, both at the type and the member level.
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 2008 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 2008, 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. For example, given this 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 2008 imports it exactly as defined, while SvcUtil imports it as published:
namespace MyOtherNamespace
{
[DataContract]
struct Contact
{...}
}
When using Visual Studio 2008, 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. If the original
service-side definition applied the DataMember
attribute on fields directly, the imported type definition will have properties accessing
fields whose names will be the names of the data members, suffixed with Field
. 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.
Even if the DataMember
attribute on the service
side is applied on a private field or property, as shown here:
[DataContract] struct Contact { [DataMember] string FirstName {get;set;} [DataMember] string LastName; }
the imported definition will have a public property instead.
If the DataMember
attribute is applied on a
property as part of the service-side data contract, the imported definition will have an
identical set of properties. The client-side properties will wrap a field named after the
property, suffixed by Field
. For example, given this
service-side data contract:
[DataContract] public partial struct Contact { string m_FirstName; string m_LastName; [DataMember] public string FirstName { get { return m_FirstName; } set { m_FirstName = value; } } [DataMember] public string LastName {get;set;} }
the imported 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; } } }
When the DataMember
attribute is applied on a
property (either on 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.
Do not apply the DataMember
attribute both on a
property and on 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.
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 stringm_
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.
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.
Service Pack 1 for .NET 3.5 introduced 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.
Inferred data contracts are sometimes called POCO, or Plain Old CLR Object.
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.
When you define a data contract, you can apply the DataMember
attribute on members that are themselves data contracts, as shown
in Example 3-3.
Example 3-3. 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.
.NET 2.0 introduced 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 3-4.
Example 3-4. 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);
This is required because internally WCF still uses reflection and delegates to
subscribe to and invoke the event-handling methods. If the serialization event attributes
(defined in the System.Runtime.Serialization
namespace)
are applied on methods with incompatible signatures, WCF will throw an exception.
StreamingContext
is a structure that is used to
inform 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 3-2 is an activity diagram depicting the order in which events are raised during serialization.
WCF first raises the serializing event, thus invoking the corresponding event handler. Next, WCF serializes the object, and finally the serialized event is raised and its event handler is invoked.
Figure 3-3 is an activity diagram depicting the order in which deserialization events are raised. WCF first raises the deserializing event, thus invoking the corresponding event handler. Next, WCF deserializes the object, and finally the deserialized event is raised and its event handler is invoked.
Note that in order to call the deserializing event-handling method, WCF has to first construct an object—however, it does so without ever calling your data contract class's default constructor.
WCF does not allow you to apply the same serialization event attribute on multiple methods of the data contract type. This is somewhat regrettable because it precludes support for partial types, where each part deals with its own serialization events.
Since no constructor calls are ever made during deserialization, the deserializing event-handling method is logically your deserialization constructor. It is intended for performing some custom pre-deserialization steps—typically, initialization of class members not marked as data members. Any value settings on members marked as data members will be in vain, because WCF will set those members again during deserialization, using values from the message. Other steps you can take in the deserializing event-handling method are setting specific environment variables (such as thread local storage), performing diagnostics, or signaling some global synchronization events. I would even go as far as to say that if you do provide such a deserializing event-handling method, you should have both the default constructor and the event handler call the same helper method, so that anyone instantiating the type using regular .NET will perform exactly the same steps that you do and you will have a single place to maintain that code:
[DataContract]
class MyClass
{
public MyClass( )
{
OnDeserializing( );
}
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
OnDeserializing( );
}
void OnDeserializing( )
{...}
}
The deserialized event lets your data contract initialize or reclaim non-data
members, while utilizing already deserialized values. Example 3-5 demonstrates this point, using the
deserialized event to initialize a database connection. Without the event, the data
contract will not be able to function properly—since the constructor is never called, it
will have a null
for the connection object.
When adding a service reference in Visual Studio 2008, you must provide a unique new namespace for each service reference. The imported types will be defined in that new namespace. This presents a problem when adding references for two different services that share the same data contract, since you will get two distinct types, in two different namespaces, representing the same data contract.
By default, however, if any of the assemblies referenced by the client has a data contract type that matches a data contract type already exposed in the metadata of the referenced service, Visual Studio 2008 will not import that type again. It is worth emphasizing again that the existing data contract reference must be in another referenced assembly, not in the client project itself. This limitation may be addressed in a future release of Visual Studio, but for now, the workaround and best practice is obvious: factor all of your shared data contracts to a designated class library, and have all clients reference that assembly. You can then control and configure which referenced assemblies (if any) to consult regarding those shared data contracts via the advanced settings dialog for the service reference (shown in Figure 1-10). The "Reuse types in referenced assemblies" checkbox is checked by default, but you can turn off this feature if you so desire. Despite its name, it will share only data contracts, not service contracts. Using the radio buttons underneath it, you can also instruct Visual Studio 2008 to reuse data contracts across all referenced assemblies, or restrict the sharing to specific assemblies by selecting them in the list.
3.14.252.56