views:

233

answers:

3

Dear ladies and sirs.

I would like to formulate a contrived scenario, which nevertheless has firm actual basis. Imagine a collection type COuter, which is a wrapper around an instance of another collection type CInner. Both implement IList (never mind the T).

Furthermore, a COuter instance is buried inside some object graph, the root of which (let us refer to it as R) is returned from a WCF service method.

My question is how can I customize the WCF serialization process, so that when R is returned, the request to serialize the COuter instance will be routed through my code, which will extract CInner and pass it to the serializer instead. Thus the receiving end still gets R, only no COuter instance is found in the object graph.

I hoped that http://stackoverflow.com/questions/2220516/how-does-wcf-serialize-the-method-call will contain the answer, unfortunately the article mentioned there (http://msdn.microsoft.com/en-us/magazine/cc163569.aspx) only barely mentions that advanced serialization scenarios are possible using IDataContractSurrogate interface, but no details are given. I am, on the other hand, would really like to see a working example.

Thank you very much in advance.

EDIT

I have created a trivial WCF sample, which demonstrates the issue. The archive is located here - https://docs.google.com/leaf?id=0B2pbsdBJxJI3NzFiNjcxMmEtMTM5Yy00MWY2LWFiMTUtNjJiNjdkYTU1ZTk4&sort=name&layout=list&num=50

It contains three small projects:

  • HelloServiceAPI - contains the service interface and the argument types
  • Host - the HelloService host
  • Client - a simple console client.

The service defines one method, which returns an instance of the HelloServiceResult type, which contains a reference to COuterList type, which wraps CInnerList type. The reference is specified as IMyListInterface, where both COuterList and CInnerList implement this interface. What I need is that when the result is serialized before being transmitted to the client, the COuterList reference be replaced with the wrapped CInnerList reference. I know this can be done by utilizing the existing abilities of WCF, I just do not know how.

A: 

Hi,

Have you tried the good old OnSerializingAttribute?

[Serializable]
[KnownType(typeof(COuterList))]
public class HelloServiceResult
{
    public IMyListInterface List;

    [OnSerialized]
    void OnSerializing(StreamingContext context)
    {
        if (List is COuterList)
        {
            List = ((List as COuterList).InnerList as CInnerList);
        }
    }
}
decyclone
This is not good. First, this code will have to be in each and every class containing an IMyListInterface instance. True, in my example there is just one, but like I said it is a contrived example. More than that, each and every class containing an IMyListInterface instance will have to be compiled twice - once for the server with the OnSerializing method and the second time - without, for the client, without the OnSerializing method, because it would just not compile - COuterList is unknown on the client. So, we have partial types and two types of builds.
mark
There is more. OnSerializing method has just changed the state of the object, which is wrong - the server side does require COuterList instances. It is the client side, which must not know it. Again, my example is contrived, so it uses the KnownType attribute. The real application does not use it at all.
mark
+3  A: 

Here is how you implement your own Surrogate:

class YourCustomTypeSurrogate : IDataContractSurrogate
{
    public Type GetDataContractType(Type type)
    {
        // Just for reference
        //if (typeof(OldType).IsAssignableFrom(type))
        //{
        //    return typeof(NewType);
        //}
        return type;
    }
    public object GetObjectToSerialize(object obj, Type targetType)
    {
        // This method is called on serialization.
        //if (obj is OldType)
        //{
        //    // ... use the XmlSerializer to perform the actual serialization.
        //    NewType newObj = new NewType();
        //    return newObj;
        //}
        return obj;
    }
    public object GetDeserializedObject(object obj, Type targetType)
    {
        // This method is called on deserialization.
        // If PersonSurrogated is being deserialized...
        //if (obj is NewType)
        //{
        //    OldType newObj = new OldType();
        //    return newObj;
        //}
        return obj;
    }
    public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
    {
        // This method is called on schema import.

        //if (typeNamespace.Equals("Your Type Namespace"))
        //{
        //    if (typeName.Equals("NewType"))
        //    {
        //        return typeof(OldType);
        //    }
        //}
        return null;
    }

    public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
    {
        // Not used in this sample.
        // You could use this method to construct an entirely 
        // new CLR type when a certain type is imported, or modify a generated
        // type in some way.
        return typeDeclaration;
    }


    public object GetCustomDataToExport(Type clrType, Type dataContractType)
    {
        // Not used in this sample
        return null;
    }

    public object GetCustomDataToExport(System.Reflection.MemberInfo memberInfo, Type dataContractType)
    {
        // Not used in this sample
        return null;
    }

    public void GetKnownCustomDataTypes(Collection<Type> customDataTypes)
    {
        // Not used in this sample
    }
}

Then you create a custom Serializer Operation Behavior :

public class CustomDataContractSerializerOperationBehavior : DataContractSerializerOperationBehavior
{
    public CustomDataContractSerializerOperationBehavior(OperationDescription operationDescription) : base(operationDescription) { }

    public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
    {
        return new DataContractSerializer(
            type /*typeof OldType*/,
            knownTypes,
            int.MaxValue /*maxItemsInObjectGraph */,
            false /*ignoreExtensionDataObject*/,
            true /*preserveObjectReferences*/,
            new YourCustomTypeSurrogate());
    }

    public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
    {
        return new DataContractSerializer(
            type /*typeof OldType*/,
            knownTypes,
            int.MaxValue /*maxItemsInObjectGraph */,
            false /*ignoreExtensionDataObject*/,
            true /*preserveObjectReferences*/,
            new YourCustomTypeSurrogate());
    }
}

After that, you create an attribute to apply the above operation behavior to an operation contract :

public class CustomDataContractFormatAttribute : Attribute, IOperationBehavior
{
    public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters)
    {
    }

    public void ApplyClientBehavior(OperationDescription description, ClientOperation proxy)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }

    public void ApplyDispatchBehavior(OperationDescription description, DispatchOperation dispatch)
    {
        ReplaceDataContractSerializerOperationBehavior(description);
    }

    public void Validate(OperationDescription description)
    {
    }

    private static void ReplaceDataContractSerializerOperationBehavior(OperationDescription description)
    {
        DataContractSerializerOperationBehavior dcs = description.Behaviors.Find<DataContractSerializerOperationBehavior>();

        if (dcs != null)
            description.Behaviors.Remove(dcs);

        description.Behaviors.Add(new CustomDataContractSerializerOperationBehavior(description));
    }
}

And finally, you apply this Attribute to an operation :

    [OperationContract]
    [CustomDataContractFormat]
    void DoWork();

If you want to apply this to whole service, then you customize Service Behavior instead of Operation Behavior.

Here are the references that were used to create this example :

http://msdn.microsoft.com/en-us/library/system.runtime.serialization.idatacontractsurrogate.aspx

http://www.danrigsby.com/blog/index.php/2008/03/07/xmlserializer-vs-datacontractserializer-serialization-in-wcf/

http://www.danrigsby.com/blog/index.php/2008/04/10/specifying-a-different-serializer-per-endpoint-in-wcf/

http://social.msdn.microsoft.com/forums/en-US/wcf/thread/e4d55f3f-86d1-441d-9187-64fbd8ab2b3d/

decyclone
Excellent. Thanks. I will mark this as the answer, unless I get a more complete reply. Anyway, if I understand it correctly, this works fine with the DataContractSerializer, but not with the NetDataContractSerializer. Is there a way to do the same with NetDataContractSerializer and only on the server side, i.e. the client side does not have to know anything about it?
mark
I don't see any reason why it should not work with `NetDataContractSerializer`. This method seems to be "serializer independent". Do you get any specific error while using `NetDataContractSerializer`?
decyclone
http://social.msdn.microsoft.com/Forums/en-US/wcf/thread/7954478c-bbea-43a5-baac-453bae51e6b7 explains it clearly. However, the problem is that one must use ISerializationSurrogate on both ends - server and the client, whereas IDataContractSurrogate can be used on one end only and this is exactly what I am looking for with NetDataContractSerializer.
mark
A: 

I am writing a WCF client to consume a web service in SAP-PI. The SAP web service has a (SAP)DateTime data type, and this web service sometimes just returns only the year, e.g. "2010".

My problem is that if this property is not set, it passes back "0000" as the value for this (SAP)DateTime argument.

The data type that was generated by the WCF proxy code uses a (.Net)DateTime object. The earliest date a .Net DateTime instance can represent is 1/1/0001, so when it tries to deserialize "0000" it blows up.

So I am writing a custom serializer using the excellent example code provided on this page by decyclone on 6/23/? at 9:03.

But the I realized that (as I understand it), decyclone's example is for use when create a WCF hosted web service, not consuming someone elses web service.

So help me understand what I need to do to accomplish this. My thoughts are:

  1. I will still need write the IDataContractSurrogate implementation class for the (SAP)DateTime type.

  2. I will still need to write a custom DataContractSerializerOperationBehavior class.

  3. Since I will not be using an attribute to decorate anything, I will not need to implement the CustomDataContractFormatAttribute class, will I? (I am not sure on this one).

  4. How do I connect the two classes I do implement (steps 1 and 2 of this post) so that when WCF receives the return response from the SAP web service method I have called, it will know to use my IDataContractSurrogate implementation instead of the default one that was generated in the proxy?

Any help or links to other references will be much appreciated!

Thanks,

Brad

--- Update ---

I think I may have found the documentation I need to figure this out:

http://msdn.microsoft.com/en-us/library/ms733064.aspx

I will let you know...

DiverBW