Data Contract Attributes

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.

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:

[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.

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.

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.

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.

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]
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.

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

Events raised during serialization

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

Events raised during deserialization

Figure 3-3. Events raised during deserialization

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.

Warning

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.

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 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()
   {...}
}

Using the deserialized event

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.

Example 3-5. Initializing non-serializable resources using the deserialized event

[DataContract]
class MyDataContract
{
   IDbConnection m_Connection;

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

Shared Data Contracts

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.130.201