Section 3.2. Data Contract Attributes


3.2. Data Contract Attributes

While the Serializable attribute is workable, it is inadequate for service-oriented interaction between clients and services. It denotes all members in the type as serializable and therefore part of the data schema for that type. It is much better 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 does not offer clean separation between the serviceness aspect of the type (ability to use it as a WCF operation parameter) and the ability to serialize it. The attribute offers no support for aliasing type name 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 any versioning information is supposedly captured by the formatters. Consequently, it is difficult to deal with versioning over time.

Yet again, the solution is to come up with new WCF 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)] 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 merely 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)] 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:

 [DataContract] struct Contact {    string m_FirstName;    string m_LastName;    [DataMember]    public string FirstName    {       get       {...}       set       {...}     }    [DataMember]    public string LastName    {       get       {...}       set       {...}     } } 

Similar to service contracts, the visibility of the data members or the data contract itself is of no consequence to WCF. You can include internal types with private data members in the data contract:

 [DataContract] struct Contact {    [DataMember]    string m_FirstName;    [DataMember]    string m_LastName; } 

Most of the text 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 level.


3.2.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 imports the definition of the data contract, the client will end up with an equivalent definition, but not an identical one. The imported definition will maintain the original type designation of a class or a structure. In addition, unlike a service contract, by default the important definition will maintain the original type 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 and provide an alternative namespace for the data contract, you can assign a value to the Namespace property of the DataContract attribute. For example, given this service-side definition:

 namespace MyNamespace {    [DataContract(Namespace = "MyOtherNamespace")]    struct Contact    {...} } 

The imported definition will be:

 namespace MyOtherNamespace {    [DataContract]    struct Contact    {...} } 

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, then the imported type definition will have properties accessing fields whose names will be the name of the data member 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:

 [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       {          return m_LastName;       }       set       {          m_LastName = value;       }    } } 

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 fieldthis will result in a duplication of the members on the importing side.


It is important to realize that the way described so far for utilizing the DataMember attribute applies both for 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 serialize and send parameters to the service, or deserialize and use the returned values 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 on, even to use nonequivalent data contracts. The client controls and configures its data contracts independently of the service.

3.2.2. Data Contract 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 fields only, it would be as if every serializable member, 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    } } 

In much the same way, the client can use the Serializable attribute on its data contract and have the wire representationthat is, the way it is marshaledbe the same as just described.

A type marked only with the DataContract attribute cannot be serialized using the legacy formatters. If you want to serialize the type, you can apply both the DataContract attribute and the Serializable attribute. The wire representation of such a type is as if only the DataContract attribute was applied, and you still need to use the DataMember attribute on the members.


Data Contract and XML Serialization

.NET offers yet another serialization mechanismraw XML serialization, using a dedicated set of attributes. When you're dealing with a data type that requires explicit control over the 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 operations on 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 caution with that switch, because it will affect all data contracts, including those that do not require explicit control over the XML serialization.


3.2.3. 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 3-3.

Example 3-3. A composite data contract

 [DataContract] struct 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 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 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 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.

3.2.4. Data Contract Events

.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 delegates to subscribe and invoke the event-handling methods. If the attributes are applied on methods with incompatible signatures, WCF will throw an exception.

StreamingContext is a structure informing the type of why it is being serialized, but it can be ignored for WCF data contracts. The event attributes are defined in the System.Runtime.Serialization namespace.

As the attribute names imply, the OnSerializing attribute designates a method handling the serializing event, and the OnSerialized attribute designates a method handling the serialized event. Similarly, the OnDeserializing attribute designates a method handling the deserializing event, and the OnDeserialized attribute designates a method handling the deserialized event. Figure 3-2 is an activity diagram depicting the order in which events are raised during serialization.

Figure 3-2. Events 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.

Figure 3-3. Events during deserialization


Note that in order to call the deserializing event-handling method, WCF has to first construct an objecthowever, it does so without ever calling your data contract class 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 regretful because it precludes support for partial types, where each part deals with its own serialization events.


3.2.4.1. Using the deserializing event

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 stepstypically, initialization of class members not marked as data members. Any value settings done 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.

3.2.4.2. Using the deserialized event

The deserialized event lets your data contract initialize or reclaim nondata members, while using already deserialized values. Example 3-5 demonstrates this point: it uses the event to initialize a database connection. Without the event, the data contract will not be able to function properly.

Example 3-5. Initializing nonserializable resources using the deserialized event

 [DataContract] class MyDataContract {    IDbConnection m_Connection;    [OnDeserialized]    void OnDeserialized(StreamingContext context)    {       m_Connection = new SqlConnection(...);    }    /* Data members */ } 




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