Data Contract Hierarchy



Data Contract Hierarchy

Your data contract class may be the 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 attribute in the class hierarchy:

[Serializable]
class Contact
{...}

[DataContract]
class Customer : Contact
{..}

But typically the Serializable attribute will be at the root of the class hierarchy, if 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 making use of the subclass in a service contract:

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

Known Types

While languages such as C# let you substitute a subclass for a base class, 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 fails at runtime:

Contact contact = new Customer( );
contact.FirstName = "Juval";
contact.LastName = "Lowy";

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

The reason is that when you pass in a Customer instead of a Contact as in the previous example, the service does not know how to deserialize the Contact object it receivedit does not know about the Customer object.

Much the same way, 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 can pass the subclass instead of the base class. If the client also applies the KnownType attribute on its copy of the base class, then it can 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, when the ServiceKnownType attribute is applied 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 on 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( );
}

Do not apply the ServiceKnownType attribute on the service class itself. Although it compiles, it is only available 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 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 reflect correctly 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 both the KnownType and the 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
{...}

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 Figure. You need to provide not just the type name but also the full name of their containing assemblies.

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>

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

Object 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 data members in data contracts, 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 exported service contract will be object-based, and will include the data contract subclass or struct without the derivation:

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

[ServiceContract]
public 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 have the ServiceKnownType attribute always 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. You can manually rework the imported definition to have only the required ServiceKnownType attributes:

[DataContract]
class Contact
{...}

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

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

If you have the definition of the base interface on the client side, you can use that instead of object, for 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]
public 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]
public interface IContactManager
{
    [OperationContract]
    void AddContact(Contact contact);

    [OperationContract]
    Contact[] GetContacts( );
}