Data Contract Hierarchy

Your data contract class may be a subclass of another data contract class. WCF requires that every level in the class hierarchy explicitly opt in for a given data contract, because the DataContract attribute is not inheritable:

[DataContract]
class Contact
{
   [DataMember]
   public string FirstName;

   [DataMember]
   public string LastName;
}
[DataContract]
class Customer : Contact
{
   [DataMember]
   public int OrderNumber;
}

Failing to designate every level in the class hierarchy as serializable or as a data contract will result in an InvalidDataContractException at the service load time. WCF lets you mix the Serializable and DataContract attributes in the class hierarchy:

[Serializable]
class Contact
{...}

[DataContract]
class Customer : Contact
{...}

However, the Serializable attribute will typically be at the root of the class hierarchy, if it appears at all, because new classes should use the DataContract attribute. When you export a data contract hierarchy, the metadata maintains the hierarchy, and all levels of the class hierarchy are exported when you make use of the subclass in a service contract:

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   void AddCustomer(Customer customer); //Contact is exported as well
   ...
}

Known Types

In traditional object-oriented programming, a reference to a subclass is also a reference to its base class, so the subclass maintains an Is-A relationship with its base class. Any method that expects a reference to a base class can also accept a reference to its subclass. This is a direct result of the way the compiler spans the state of the subclass in memory, by appending it right after the base class section.

While languages such as C# let you substitute a subclass for a base class in this manner, this is not the case with WCF operations. By default, you cannot use a subclass of a data contract class instead of its base class. Consider this service contract:

[ServiceContract]
interface IContactManager
{
   //Cannot accept Customer object here:
   [OperationContract]
   void AddContact(Contact contact);

   //Cannot return Customer objects here:
   [OperationContract]
   Contact[] GetContacts(  );
}

Suppose the client defined the Customer class as well:

[DataContract]
class Customer : Contact
{
   [DataMember]
   public int OrderNumber;
}

While the following code compiles successfully, it will fail at runtime:

Contact contact = new Customer(  )
                      {
                         ...
                      };

ContactManagerClient proxy = new ContactManagerClient(  );
//Service call will fail:
proxy.AddContact(contact);
proxy.Close(  );

The reason is that you are not actually passing an object reference; you are instead passing the object's state. When you pass in a Customer instead of a Contact, as in the previous example, the service does not know it should deserialize the Customer portion of the state.

Likewise, when a Customer is returned instead of a Contact the client does not know how to deserialize it, because all it knows about are Contacts, not Customers:

/////////////////////////// Service Side //////////////////////////////
[DataContract]
class Customer : Contact
{
   [DataMember]
   public int OrderNumber;
}
class CustomerManager : IContactManager
{
   List<Customer> m_Customers = new List<Customer>(  );

   public Contact[] GetContacts(  )
   {
      return m_Customers.ToArray(  );
   }
   //Rest of the implementation
}
/////////////////////////// Client Side //////////////////////////////
ContactManagerClient proxy = new ContactManagerClient(  );
//Call will fail:
Contact[] contacts = proxy.GetContacts(  );
proxy.Close(  );

The solution is to explicitly tell WCF about the Customer class using the KnownTypeAttribute, defined as:

[AttributeUsage(AttributeTargets.Struct|AttributeTargets.Class,
                AllowMultiple = true)]
public sealed class KnownTypeAttribute : Attribute
{
   public KnownTypeAttribute(Type type);
   //More members
}

The KnownType attribute allows you to designate acceptable subclasses for the data contract:

[DataContract]
[KnownType(typeof(Customer))]
class Contact
{...}

[DataContract]
class Customer : Contact
{...}

On the host side, the KnownType attribute affects all contracts and operations using the base class, across all services and endpoints, allowing it to accept subclasses instead of base classes. In addition, it includes the subclass in the metadata, so that the client will have its own definition of the subclass and will be able to pass the subclass instead of the base class. If the client also applies the KnownType attribute on its copy of the base class, it can in turn receive the known subclass back from the service.

Service Known Types

The downside of using the KnownType attribute is that it may be too broad in scope. WCF also provides the ServiceKnownTypeAttribute, defined as:

[AttributeUsage(AttributeTargets.Interface|
                AttributeTargets.Method   |
                AttributeTargets.Class,
                AllowMultiple = true)]
public sealed class ServiceKnownTypeAttribute : Attribute
{
   public ServiceKnownTypeAttribute(Type type);
   //More members
}

Instead of using the KnownType attribute on the base data contract, you can apply the ServiceKnownType attribute on a specific operation on the service side. Then, only that operation (across all supporting services) can accept the known subclass:

[DataContract]
class Contact
{...}

[DataContract]
class Customer : Contact
{...}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   [ServiceKnownType(typeof(Customer))]
   void AddContact(Contact contact);

   [OperationContract]
   Contact[] GetContacts(  );
}

Other operations cannot accept the subclass.

When the ServiceKnownType attribute is applied at the contract level, all the operations in that contract can accept the known subclass, across all implementing services:

[ServiceContract]
[ServiceKnownType(typeof(Customer))]
interface IContactManager
{
   [OperationContract]
   void AddContact(Contact contact);

   [OperationContract]
   Contact[] GetContacts(  );
}

Warning

Do not apply the ServiceKnownType attribute on the service class itself. Although the code will compile, this will have an effect only when you don't define the service contract as an interface (something I strongly discourage in any case). If you apply the ServiceKnownType attribute on the service class while there is a separate contract definition, it will have no effect.

Whether you apply the ServiceKnownType attribute at the operation or the contract level, the exported metadata and the generated proxy will have no trace of it and will include the KnownType attribute on the base class only. For example, given this service-side definition:

[ServiceContract]
[ServiceKnownType(typeof(Customer))]
interface IContactManager
{...}

The imported definition will be:

[DataContract]
[KnownType(typeof(Customer))]
class Contact
{...}
[DataContract]
class Customer : Contact
{...}
[ServiceContract]
interface IContactManager
{...}

You can manually rework the client-side proxy class to correctly reflect the service-side semantic by removing the KnownType attribute from the base class and applying the ServiceKnownType attribute to the appropriate level in the contract.

Multiple Known Types

You can apply the KnownType and ServiceKnownType attributes multiple times to inform WCF about as many known types as required:

[DataContract]
class Contact
{...}

[DataContract]
class Customer : Contact
{...}

[DataContract]
class Person : Contact
{...}

[ServiceContract]
[ServiceKnownType(typeof(Customer))]
[ServiceKnownType(typeof(Person))]
interface IContactManager
{...}

The WCF formatter uses reflection to collect all the known types of the data contracts, and then examines the provided parameter to see if it is of any of the known types.

Note that you must explicitly add all levels in the data contract class hierarchy. Adding a subclass does not add its base class(es):

[DataContract]
class Contact
{...}

[DataContract]
class Customer : Contact
{...}

[DataContract]
class Person : Customer
{...}

[ServiceContract]
[ServiceKnownType(typeof(Customer))]
[ServiceKnownType(typeof(Person))]
interface IContactManager
{...}

Configuring Known Types

The main downside of the known types attributes is that they require the service or the client to know in advance about all possible subclasses the other party may want to use. Adding a new subclass necessitates changing the code, recompiling, and redeploying. To alleviate this, WCF lets you configure the known types in the service's or client's config file, as shown in Example 3-6. You need to provide not just the type names, but also the names of their containing assemblies.

Example 3-6. Known types in config file

<system.runtime.serialization>
   <dataContractSerializer>
      <declaredTypes>
         <add type = "Contact,Host,Version = 1.0.0.0,Culture = neutral,
                      PublicKeyToken = null">
            <knownType type = "Customer,MyClassLibrary,Version = 1.0.0.0,
                               Culture = neutral,PublicKeyToken = null"/>
         </add>
      </declaredTypes>
   </dataContractSerializer>
</system.runtime.serialization>

When not relying on string name or assembly version resolution, you can just use the assembly-friendly name:

<add type = "Contact,Host">
   <knownType type = "Customer,MyClassLibrary"/>
</add>

Including the known types in the config file has the same effect as applying the KnownType attribute on the data contract, and the published metadata will include the known types definition.

Interestingly enough, using a config file to declare a known type is the only way to add a known type that is internal to another assembly.

Objects and Interfaces

The base type of a data contract class or a struct can be an interface:

interface IContact
{
   string FirstName
   {get;set;}
   string LastName
   {get;set;}
}
[DataContract]
class Contact : IContact
{...}

You can use such a base interface in your service contract, or as a data member in a data contract, as long as you use the ServiceKnownType attribute to designate the actual data type:

[ServiceContract]
[ServiceKnownType(typeof(Contact))]
interface IContactManager
{
   [OperationContract]
   void AddContact(IContact contact);

   [OperationContract]
   IContact[] GetContacts(  );
}

You cannot apply the KnownType attribute on the base interface, because the interface itself will not be included in the exported metadata. Instead, the imported service contract will be object-based, and it will include the data contract subclass or struct without the derivation:

//Imported definitions:
[DataContract]
class Contact
{...}

[ServiceContract]
interface IContactManager
{
    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    [ServiceKnownType(typeof(object[]))]
    void AddContact(object contact);

    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    [ServiceKnownType(typeof(object[]))]
    object[] GetContacts(  );
}

The imported definition will always have the ServiceKnownType attribute applied at the operation level, even if it was originally defined at the scope of the contract. In addition, every operation will include a union of all the ServiceKnownType attributes required by all the operations, including a redundant service known type attribute for the array. These are relics from a time when these definitions were required in a pre-released version of WCF.

You can manually rework the imported definition to have only the required ServiceKnownType attributes:

[DataContract]
class Contact
{...}

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   [ServiceKnownType(typeof(Contact))]
   void AddContact(object contact);

   [OperationContract]
   [ServiceKnownType(typeof(Contact))]
   object[] GetContacts(  );
}

Or better yet, if you have the definition of the base interface on the client side, or if you refactor that definition, you can use that instead of object. This gives you an added degree of type safety, as long as you add a derivation from the interface to the data contract:

[DataContract]
class Contact : IContact
{...}

[ServiceContract]
interface IContactManager
{
    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    void AddContact(IContact contact);

    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    IContact[] GetContacts(  );
}

However, you cannot replace the object in the imported contract with the concrete data contract type, because it is no longer compatible:

//Invalid client-side contract
[ServiceContract]
interface IContactManager
{
    [OperationContract]
    void AddContact(Contact contact);

    [OperationContract]
    Contact[] GetContacts(  );
}
..................Content has been hidden....................

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