views:

2426

answers:

3

My client/server application is using WCF for communication, which has been great. However one shortcoming of the current architecture is that I must use known type configuration for certain transmitted types. I'm using an in-house Pub/Sub mechanism and this requirement is unavoidable.

The problem is that it's easy to forget to add the known type, and if you do, WCF fails silently with few clues as to what's going wrong.

In my application, I know the set of types that are going to be sent. I would like to perform the configuration programmatically, rather than declaratively through the App.config file which currently contains something like this:

<system.runtime.serialization>
  <dataContractSerializer>
    <declaredTypes>
      <add type="MyProject.MyParent, MyProjectAssembly">
        <knownType type="MyProject.MyChild1, MyProjectAssembly"/>
        <knownType type="MyProject.MyChild2, MyProjectAssembly"/>
        <knownType type="MyProject.MyChild3, MyProjectAssembly"/>
        <knownType type="MyProject.MyChild4, MyProjectAssembly"/>
        <knownType type="MyProject.MyChild5, MyProjectAssembly"/>
      </add>
    </declaredTypes>
  </dataContractSerializer>
</system.runtime.serialization>

Instead, I'd like to do something like this:

foreach (Type type in _transmittedTypes)
{
    // How would I write this method?
    AddKnownType(typeof(MyParent), type);
}

Can someone please explain how I might do this?

EDIT Please understand that I'm trying to set the known types dynamically at run time rather than declaratively in config or using attributes in the source code.

This is basically a question about the WCF API, not a style question.

EDIT 2 This MSDN page page states:

You can also add types to the ReadOnlyCollection, accessed through the KnownTypes property of the DataContractSerializer.

Unfortunately that's all it says and it doesn't make terribly much sense given that KnownTypes is a readonly property, and the property's value is a ReadOnlyCollection.

+11  A: 

Add [ServiceKnownType] to your [ServiceContract] interface:

[ServiceKnownType("GetKnownTypes", typeof(KnownTypesProvider))]

then create a class called KnownTypesProvider:

internal static class KnownTypesProvider
{
    public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
    {
         // collect and pass back the list of known types
    }
}

and then you can pass back whatever types you need.

Miki Watts
Perhaps my question wasn't clear. This is not 'programmatically' -- it's still declaratively. I need the ability to *add* known types, not *get* them, at run time.
Drew Noakes
@ Drew Noakes - Huh? In the GetKnownTypes method, which is just code, you can return the known types at that point in time. The attribute is just there to tell WCF what method to call to get the known types. This is as programmatically as you can have it in WCF, I think (short of programmatically editing the config file and reloading it).
Kurt Schelfthout
Agreed with Miki and Kurt, this is as good as you're going to get in WCF.
unforgiven3
I apologise. This is actually a valid answer, I just misread it as a means of accessing the existing known types. Looking at it again I've no idea what I was thinking.
Drew Noakes
@Drew: this answers shows exactly that. The types returned by the GetKnownTypes method are added to the list of known types supported by the service. You can use reflection to build up that type list, based on whatever criteria suits you.Keep in mind that any *new* types require regenerating the client proxy in order to use them. I don't think it's possible to work around this.
Dan C.
@DanC - The list of known types must be available on the sender's side as well, so presumably the service host must be recreated for *new* types too.
Drew Noakes
Correct (however you can work around that, provided you can "detect" when you get a new type and recreate the host).At any rate, I used the above pattern for loading the types at service startup (just wanted to get rid of having to specify all derived classes declaratively). If you want to dynamically add types while the service is running, then you're probably facing a lot more problems.
Dan C.
Just wanted to chime in again and say thanks very much for this answer. I've only just gotten around to using this and it worked perfectly first time. I edited your answer slightly to widen the return type to `IEnumerable<Type>` and indicate that the class can be internal and still work ok. Thanks again.
Drew Noakes
A: 

I don't. Plymorphism is an OO term, not SOA term, so I don't use it, and make my contracts explicit wherever possible. Take a look at this answer as well: http://stackoverflow.com/questions/416457/wcf-contract-returning-interface-could-cause-serialization-issue/421657#421657

Krzysztof Koźmic
Take a look at Exchange Web Service : http://msdn.microsoft.com/en-us/library/bb204119.aspx, it uses polymorphism heavily.This is not a simple web service to use but it is very powerfull and coarse grained.
Nicolas Dorier
I too prefer to make things explicit wherever possible.Whilst I appreciate that this might not be the best way of doing things, I made it clear that this is an in-house Pub/Sub mechanism that sends 'object' via WCF. I want to make using this system simpler as the Pub/Sub mechanism knows the types in question and could configure the underlying transport (WCF) accordingly.Why do you think I'm using SOA?
Drew Noakes
Schema supports the "base" attribute for complex type extensions, giving you polymorphism-like properties of schema. Insomuch as schema supports it, it's compatible with the technologies that typically make up a SOA. http://www.w3.org/2001/XMLSchemaOf course the word "polymorphism" doesn't apply much to SOA because polymorphism in this case refers more to implementation of a SOA, rather than SOA itself. SOA and polymorphism don't have much to do with each other.
Anderson Imes
Right. I'm not saying you CAN'T do it. I'm saying you SHOUDN'T do it, it's like putting circles into square holes. You can do it, but it feels wrong...
Krzysztof Koźmic
I have a situation where I've included class A in the same assembly as my contract. I've used DataMember to explicity identify the state of A that I'm interested in. I then have class B which subclasses A. There's nothing in B I care about from the service's standpoint, it will treat instances of B just like As. However, I have B in a different assembly and I don't want the service to depend on that assembly. Sadly, I see no way to achieve this with WCF. If my contract expects an A, why do I have to tell it about Bs? Bs should be As.
JohnOpincar
@John,Because WCF is not polymorphic. If you are stuck with that design, you can configure known types in the config file, thus removing the need of having explicit dependency between assemblies
Krzysztof Koźmic
+3  A: 

There are 2 additional ways to solve your problem:

I. Use KnownTypeAttribute(string):

[DataContract]
[KnownType("GetKnownTypes")]
public abstract class MyParent
{
    static IEnumerable<Type> GetKnownTypes()
    {
        return new Type[] { typeof(MyChild1), typeof(MyChild2), typeof(MyChild3) };
    }
}

II. Use constructor DataContractSerializer

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | 
                AttributeTargets.Interface)]
public class MyHierarchyKnownTypeAttribute : Attribute, IOperationBehavior, IServiceBehavior, IContractBehavior
{
    void IOperationBehavior.AddBindingParameters(
            OperationDescription description, 
            BindingParameterCollection parameters)
    {}

    void IOperationBehavior.ApplyClientBehavior(
            OperationDescription description, 
            System.ServiceModel.Dispatcher.ClientOperation proxy)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }
    void IOperationBehavior.ApplyDispatchBehavior(
            OperationDescription description, 
            System.ServiceModel.Dispatcher.DispatchOperation dispatch)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }
    void IOperationBehavior.Validate(OperationDescription description)
    {}


    void IServiceBehavior.AddBindingParameters(
          ServiceDescription serviceDescription,
          ServiceHostBase serviceHostBase,
          System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints,
          BindingParameterCollection bindingParameters)
    {
        ReplaceDataContractSerializerOperationBehavior(serviceDescription);
    }

    void IServiceBehavior.ApplyDispatchBehavior(
            ServiceDescription serviceDescription, 
            ServiceHostBase serviceHostBase)
    {
        ReplaceDataContractSerializerOperationBehavior(serviceDescription);
    }

    void IServiceBehavior.Validate(ServiceDescription serviceDescription,
            ServiceHostBase serviceHostBase)
    {}

    void IContractBehavior.AddBindingParameters(
            ContractDescription contractDescription,
            ServiceEndpoint endpoint, 
            BindingParameterCollection bindingParameters)
    {}

    void IContractBehavior.ApplyClientBehavior(
            ContractDescription contractDescription,
            ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        ReplaceDataContractSerializerOperationBehavior(contractDescription);
    }

    void IContractBehavior.ApplyDispatchBehavior(
            ContractDescription contractDescription,
            ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
        ReplaceDataContractSerializerOperationBehavior(contractDescription);
    }

    void IContractBehavior.Validate(ContractDescription contractDescription,
            ServiceEndpoint endpoint)
    {}


    private static void ReplaceDataContractSerializerOperationBehavior(
            ServiceDescription description)
    {
        foreach (var endpoint in description.Endpoints)
        {
            ReplaceDataContractSerializerOperationBehavior(endpoint);
        }
    }

    private static void ReplaceDataContractSerializerOperationBehavior(
            ContractDescription description)
    {
        foreach (var operation in description.Operations)
        {
            ReplaceDataContractSerializerOperationBehavior(operation);
        }
    }

    private static void ReplaceDataContractSerializerOperationBehavior(
            ServiceEndpoint endpoint)
    {
        // ignore mexигнорируем mex-интерфейс
        if (endpoint.Contract.ContractType == typeof(IMetadataExchange))
        {
            return;
        }
        ReplaceDataContractSerializerOperationBehavior(endpoint.Contract);
    }

    private static void ReplaceDataContractSerializerOperationBehavior(
            OperationDescription description)
    {
        var behavior = 
         description.Behaviors.Find<DataContractSerializerOperationBehavior>();
        if (behavior != null)
        {
            description.Behaviors.Remove(behavior);
            description.Behaviors.Add(
                new ShapeDataContractSerializerOperationBehavior(description));
        }

    }

    public class ShapeDataContractSerializerOperationBehavior 
            : DataContractSerializerOperationBehavior
    {
        public ShapeDataContractSerializerOperationBehavior(
                OperationDescription description)
            : base(description) { }

        public override XmlObjectSerializer CreateSerializer(Type type, 
                string name, string ns, IList<Type> knownTypes)
        {
            var shapeKnownTypes = 
                new List<Type> { typeof(Circle), typeof(Square) };
            return new DataContractSerializer(type, name, ns, shapeKnownTypes);
        }

        public override XmlObjectSerializer CreateSerializer(Type type, 
                XmlDictionaryString name, XmlDictionaryString ns, 
                IList<Type> knownTypes)
        {
            //All magic here!
            var knownTypes = 
                new List<Type> { typeof(MyChild1), typeof(MyChild2), typeof(MyChild3) };
            return new DataContractSerializer(type, name, ns, knownTypes);
        }
    }
}


[ServiceContract()]
[MyHierarchyKnownTypeAttribute]
public interface IService {...}

NOTE: You must use this attribute on both sides: client side and service side!

Sergey Teplyakov
+1 for the complete code sample!
Samuel Jack