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 Data
Member
Attribute
, 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.
Data contracts are case-sensitive both at the type and member levels.
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.
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.
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.
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.
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.
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.
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 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 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);
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 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 classes 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 only a default constructor and 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 2010, you must provide a unique 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 2010 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 box 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 below it, you can also instruct Visual Studio 2010 to reuse data contracts across all referenced assemblies, or restrict the sharing to specific assemblies by selecting them in the list.
3.16.130.201