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 ... }
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 Contact
s, not
Customer
s:
/////////////////////////// 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.
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( );
}
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.
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
{...}
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.
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( ); }
3.144.31.163