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 CustomerNumber;
}
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
...
}
1. 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 CustomerNumber;
}
While the following code will compile 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 CustomerNumber;
}
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 if there are items in the list:
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.
2. 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.
3. 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, 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
{...}
4. 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 1. You need to provide not just
the type names, but also the names of their containing
assemblies.
Example 1. Known types in config file
<system.runtime.serialization> <dataContractSerializer> <declaredTypes> <add type = "Contact,MyClassLibrary,Version = 1.0.0.0,Culture = neutral, PublicKeyToken = null"> <knownType type = "Customer,MyOtherClassLibrary,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,MyClassLibrary">
<knownType type = "Customer,MyOtherClassLibrary"/>
</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.
Note: Using a config file to declare a known type is the only way to
add a known type that is internal to another assembly.
5. Data Contract Resolvers
The final technique for addressing known types would be to
do so programmatically. This is the most powerful technique, since you
can extend it to completely automate dealing with the known type issues.
This is possible using a mechanism called data contract resolvers
introduced by WCF in .NET 4.0. In essence, you are given a chance to
intercept the operation’s attempt to serialize and deserialize
parameters and resolve the known types at runtime both on the client and
service sides.
The first step in implementing a programmatic resolution is to
derive from the abstract class DataContractResolver defined as:
public abstract class DataContractResolver
{
protected DataContractResolver();
public abstract bool TryResolveType(Type type,Type declaredType,
DataContractResolver knownTypeResolver,
out XmlDictionaryString typeName,
out XmlDictionaryString typeNamespace);
public abstract Type ResolveName(string typeName,string typeNamespace,
Type declaredType,
DataContractResolver knownTypeResolver);
}
Your implementation of the TryResolveType() is
called when WCF tries to serialize a type into a message and the type
provided (the type parameter) is
different from the type declared in the operation contract (the declaredType parameter). If you want to
serialize the type, you need to provide some unique identifiers to serve
as keys into a dictionary that maps identifiers to types. WCF will
provide you those keys during deserialization so that you can bind
against that type. Note that the namespace key cannot be an empty string
or a null. While virtually any unique
string value will do for the identifiers, I recommend simply using the
CLR type name and namespace. Set the type name and namespace into the
typeName and typeNamespace out parameters.
If you return true from
TryResolveType(), the type is
considered resolved as if you had applied the KnownType attribute. If you return false, WCF fails the call. Note that TryResolveType() must resolve all known types,
even those types that are decorated with the KnownType attribute or are listed in the
config file. This presents a potential risk: it requires the resolver to
be coupled to all known types in the application and will fail the
operation call with other types that may come over time. It is therefore
preferable as a fallback contingency to try to resolve the type using
the default known types resolver that WCF would have used if your
resolver was not in use. This is exactly what the knownTypeResolver parameter is for. If your
implementation of TryResolveType()
cannot resolve the type, it should delegate to knownTypeResolver.
The ResolveName() is called
when WCF tries to deserialize a type out of a message, and the type
provided (the type parameter) is
different from the type declared in the operation contract (the declaredType parameter). In this case, WCF
provides you with the type name and namespace identifiers so that you
can map them back to a known type.
For example, consider again these two data contracts:
[DataContract]
class Contact
{...}
[DataContract]
class Customer : Contact
{...}
Example 2 lists a simple resolver
for the Customer type.
Example 2. The CustomerResolver
class CustomerResolver : DataContractResolver { string Namespace { get { return typeof(Customer).Namespace ?? "global"; } } string Name { get { return typeof(Customer).Name; } }
public override Type ResolveName(string typeName,string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) { if(typeName == Name && typeNamespace == Namespace) { return typeof(Customer); } else { return knownTypeResolver. ResolveName(typeName,typeNamespace,declaredType,null); } }
public override bool TryResolveType(Type type,Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) { if(type == typeof(Customer)) { XmlDictionary dictionary = new XmlDictionary(); typeName = dictionary.Add(Name); typeNamespace = dictionary.Add(Namespace); return true; } else { return knownTypeResolver. TryResolveType(type,declaredType,null,out typeName,out typeNamespace); } } }
|
5.1. Installing the data contract resolver
The resolver must be attached as a behavior for each
operation on the proxy or the service endpoint. For example, how you choose to
resolve a known type (be it declaratively via the KnownType attribute or programmatically with
a resolver) is a local implementation detail, on both the client and
the service sides.
In WCF, every endpoint it represented by the type ServiceEndpoint. The ServiceEndpoint has a property called Contract of the type ContractDescription:
public class ServiceEndpoint
{
public ContractDescription Contract
{get;set;}
//More members
}
ContractDescription has a
collection of operation descriptions, with an instance of OperationDescription for every operation on
the contract:
public class ContractDescription
{
public OperationDescriptionCollection Operations
{get;}
//More members
}
public class OperationDescriptionCollection :
Collection<OperationDescription>
{...}
Each OperationDescription has
a collection of operation behaviors of the type IOperationBehavior:
public class OperationDescription
{
public KeyedByTypeCollection<IOperationBehavior> Behaviors
{get;}
//More members
}
In its collection of behaviors, every operation always has a
behavior called DataContractSerializerOperationBehavior with a DataContractResolver property:
public class DataContractSerializerOperationBehavior : IOperationBehavior,...
{
public DataContractResolver DataContractResolver
{get;set}
//More members
}
The DataContractResolver
property defaults to null, but you
can set it to your custom resolver.
To install a resolver on the host side, you must iterate over
the collection of endpoints in the service description maintained by
the host:
public class ServiceHost : ServiceHostBase
{...}
public abstract class ServiceHostBase : ...
{
public ServiceDescription Description
{get;}
//More members
}
public class ServiceDescription
{
public ServiceEndpointCollection Endpoints
{get;}
//More members
}
public class ServiceEndpointCollection : Collection<ServiceEndpoint>
{...}
Suppose you have the following service definition and are using
the resolver in Example 3-7:
[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
...
}
class ContactManager : IContactManager
{...}
Example 3 shows how
to install the resolver on the host for the ContactManager service.
Example 3. Installing a resolver on the host
ServiceHost host = new ServiceHost(typeof(ContactManager)); foreach(ServiceEndpoint endpoint in host.Description.Endpoints) { foreach(OperationDescription operation in endpoint.Contract.Operations) { DataContractSerializerOperationBehavior behavior = operation.Behaviors.Find<DataContractSerializerOperationBehavior>(); behavior.DataContractResolver = new CustomerResolver(); } } host.Open();
|
On the client side, you follow similar steps, except you need to
set the resolver on the single endpoint of the proxy or the channel
factory. For example, given this proxy class definition:
class ContactManagerClient : ClientBase<IContactManager>,IContactManager
{...}
Example 4 shows how
to install the resolver on the proxy in order to call the service of
Example 3 with a known
type.
Example 4. Installing a resolver on the proxy
ContactManagerClient proxy = new ContactManagerClient();
foreach(OperationDescription operation in proxy.Endpoint.Contract.Operations) { DataContractSerializerOperationBehavior behavior = operation.Behaviors.Find<DataContractSerializerOperationBehavior>();
behavior.DataContractResolver = new CustomerResolver(); }
Customer customer = new Customer(); ...
proxy.AddContact(customer);
|
5.2. The generic resolver
Writing and installing a resolver for each type is
obviously a lot of work, requiring you to meticulously track all known
types, something that is error-prone and can quickly get out of hand
in an evolving system. To automate implementing a resolver, I wrote
the class GenericResolver defined
as:
public class GenericResolver : DataContractResolver
{
public Type[] KnownTypes
{get;}
public GenericResolver();
public GenericResolver(Type[] typesToResolve);
public static GenericResolver Merge(GenericResolver resolver1,
GenericResolver resolver2);
}
GenericResolver offers two
constructors. One constructor can accept an array of known types to
resolve. The types in the array can include bounded generic types, that is,
generic types for which you have already specified type parameters.
The parameterless constructor will automatically add as known types
all classes and structs in the calling assembly and all public classes
and structs in assemblies referenced by the calling assemblies. The
parameterless constructor will not add types originating in a .NET
Framework–referenced assembly. Note that the parameterless constructor
will also ignore generic types (since there is no way of inferring the
type parameters used in code). In addition, GenericResolver offers the Merge() static method that you can use to
merge the known types of two resolvers, returning a GenericResolver that resolves the union of
the two resolvers provided. Example 5 shows the
pertinent portion of GenericResolver without reflecting the types
in the assemblies, which has nothing to do with WCF.
Example 5. Implementing GenericResolver (partial)
public class GenericResolver : DataContractResolver { const string DefaultNamespace = "global";
readonly Dictionary<Type,Tuple<string,string>> m_TypeToNames; readonly Dictionary<string,Dictionary<string,Type>> m_NamesToType;
public Type[] KnownTypes { get { return m_TypeToNames.Keys.ToArray(); } }
//Get all types in calling assembly and referenced assemblies static Type[] ReflectTypes() {...}
public GenericResolver() : this(ReflectTypes()) {} public GenericResolver(Type[] typesToResolve) { m_TypeToNames = new Dictionary<Type,Tuple<string,string>>(); m_NamesToType = new Dictionary<string,Dictionary<string,Type>>();
foreach(Type type in typesToResolve) { string typeNamespace = GetNamespace(type); string typeName = GetName(type);
m_TypeToNames[type] = new Tuple<string,string>(typeNamespace,typeName);
if(m_NamesToType.ContainsKey(typeNamespace) == false) { m_NamesToType[typeNamespace] = new Dictionary<string,Type>(); }
m_NamesToType[typeNamespace][typeName] = type; } } static string GetNamespace(Type type) { return type.Namespace ?? DefaultNamespace; } static string GetName(Type type) { return type.Name; }
public static GenericResolver Merge(GenericResolver resolver1, GenericResolver resolver2) { if(resolver1 == null) { return resolver2; } if(resolver2 == null) { return resolver1; } List<Type> types = new List<Type>();
types.AddRange(resolver1.KnownTypes); types.AddRange(resolver2.KnownTypes);
return new GenericResolver(types.ToArray()); } public override Type ResolveName(string typeName,string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver) { if(m_NamesToType.ContainsKey(typeNamespace)) { if(m_NamesToType[typeNamespace].ContainsKey(typeName)) { return m_NamesToType[typeNamespace][typeName]; } } return knownTypeResolver. ResolveName(typeName,typeNamespace,declaredType,null); } public override bool TryResolveType(Type type,Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace) { if(m_TypeToNames.ContainsKey(type)) { XmlDictionary dictionary = new XmlDictionary(); typeNamespace = dictionary.Add(m_TypeToNames[type].Item1); typeName = dictionary.Add(m_TypeToNames[type].Item2); return true; } else { return knownTypeResolver. TryResolveType(type,declaredType,null,out typeName, out typeNamespace); } } }
|
The most important members of GenericResolver are the m_TypeToNames and the m_NamesToType
dictionaries. m_TypeToNames maps a
type to a tuple of its name and namespace. m_NamesToType maps a type namespace and name
to the actual type. The constructor that takes the array of types
initializes those two dictionaries. The Merge() method uses the helper property
KnownTypes[] to merge the two
resolvers. The TryResolveType()
method uses the provided type as a key into the m_TypeToNames dictionary to read
the type’s name and namespace. The ResolveName() method uses the provided
namespace and name as keys into the m_NamesToType dictionary to return the
resolved type.
5.3. Installing the generic resolver
While you could use tedious code similar to Example 3-8 and Example 3-9 to install GenericResolver, it is best to streamline it
with extension methods. To that end, use my AddGenericResolver() methods of GenericResolverInstaller defined as:
public static class GenericResolverInstaller
{
public static void AddGenericResolver(this ServiceHost host,
params Type[] typesToResolve);
public static void AddGenericResolver<T>(this ClientBase<T> proxy,
params Type[] typesToResolve) where T : class;
public static void AddGenericResolver<T>(this ChannelFactory<T> factory,
params Type[] typesToResolve) where T : class;
}
The AddGenericResolver()
method accepts a params array of
types, which means an open-ended, comma-separated list of types. If
you do not specify types, that will make AddGenericResolver() add as known types all
classes and structs in the calling assembly plus the public classes
and structs in referenced assemblies. For example, given these known
types:
[DataContract]
class Contact
{...}
[DataContract]
class Customer : Contact
{...}
[DataContract]
class Employee : Contact
{...}
Example 6 shows several
examples of using the AddGenericResolver() extension
method.
Example 6. Installing GenericResolver
//Host side
ServiceHost host1 = new ServiceHost(typeof(ContactManager)); //Resolve all types in this and referenced assemblies host1.AddGenericResolver(); host1.Open();
ServiceHost host2 = new ServiceHost(typeof(ContactManager)); //Resolve only Customer and Employee host2.AddGenericResolver(typeof(Customer),typeof(Employee)); host2.Open();
ServiceHost host3 = new ServiceHost(typeof(ContactManager)); //Can call AddGenericResolver() multiple times host3.AddGenericResolver(typeof(Customer)); host3.AddGenericResolver(typeof(Employee)); host3.Open();
//Client side
ContactManagerClient proxy = new ContactManagerClient(); //Resolve all types in this and referenced assemblies proxy.AddGenericResolver();
Customer customer = new Customer(); ... proxy.AddContact(customer);
|
GenericResolverInstaller not
only installs the GenericResolver,
it also tries to merge it with the old
generic resolver (if present). This means you can call the
AddGenericResolver() method multiple
times. This is handy when adding bounded generic types:
[DataContract]
class Customer<T> : Contact
{...}
ServiceHost host = new ServiceHost(typeof(ContactManager));
//Add all non-generic known types
host.AddGenericResolver();
//Add the generic types
host.AddGenericResolver(typeof(Customer<int>,Customer<string>));
host.Open();
Example 7 shows
the partial implementation of GenericResolverInstaller.
Example 7. Implementing GenericResolverInstaller
public static class GenericResolverInstaller { public static void AddGenericResolver(this ServiceHost host, params Type[] typesToResolve) { foreach(ServiceEndpoint endpoint in host.Description.Endpoints) { AddGenericResolver(endpoint,typesToResolve); } }
static void AddGenericResolver(ServiceEndpoint endpoint,Type[] typesToResolve) { foreach(OperationDescription operation in endpoint.Contract.Operations) { DataContractSerializerOperationBehavior behavior = operation. Behaviors.Find<DataContractSerializerOperationBehavior>();
GenericResolver newResolver;
if(typesToResolve == null || typesToResolve.Any() == false) { newResolver = new GenericResolver(); } else { newResolver = new GenericResolver(typesToResolve); }
GenericResolver oldResolver = behavior.DataContractResolver as GenericResolver; behavior.DataContractResolver = GenericResolver.Merge(oldResolver,newResolver); } } }
|
If no types are provided, AddGenericResolver() will use the
parameterless constructor of GenericResolver. Otherwise, it will use only
the specified types by calling the other constructor. Note the merging
with the old resolver if present.
5.4. GenericResolver and ServiceHost<T>
Using a generic resolver is great, and arguably should
always be associated with any host since you have no way of knowing in
advance all the known types your system will encounter. In that case,
instead of explicitly adding GenericResolver to the host (as in Example 3-11), you can also have a custom
host type that adds the generic resolver implicitly. For example, My
ServiceHost<T> does just that:
public class ServiceHost<T> : ServiceHost
{
protected override void OnOpening()
{
this.AddGenericResolver();
...
}
}
This means that the following code is all you will need when it
comes to known types:
ServiceHost host = new ServiceHost<ContactManager>();
host.Open();
5.5. Generic resolver attributeIf your service relies on the generic resolver by design, it is
better not to be at the mercy of the host and to declare your need for
the generic resolver at design time. To that end, I wrote the GenericResolverBehaviorAttribute:
[AttributeUsage(AttributeTargets.Class)]
public class GenericResolverBehaviorAttribute : Attribute,IServiceBehavior
{
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
ServiceHost host = serviceHostBase as ServiceHost;
host.AddGenericResolver();
}
//More members
}
This concise attribute makes the service independent of the host
and the config file:
[GenericResolverBehavior]
class ContactManager : IContactManager
{...}
GenericResolverBehaviorAttribute derives
from IServiceBehavior. IServiceBehavior is a special WCF interface
and it is the most commonly used extension in WCF. Subsequent chapters
will make extensive use of it and will discuss its various methods.
Briefly, when the host loads the service, it uses reflection to
determine if the service class has an attribute that supports IServiceBehavior and, if so, the host calls
the IServiceBehavior methods,
specifically the Validate() method,
which lets the attribute interact with the host. In the case of
GenericResolverBehaviorAttribute,
it adds the generic resolver to the host.
6. 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 if 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 not include the data contract interface:
//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 beta 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();
}