Search code examples
c#wcfdatacontractserializer

Change the response output format in XML and JSON only for webHttpBinding


I've been working on this problem for quite a long time, here are my findings and requirements:

We have two endpoints:

  • TCP endpoint
  • WebHttp endpoint

Through the WebHttp endpoint, we need to support JSON and XML but with a custom response format. This is the format needed (only JSON shown for clarity):

{
    "status": "success",
    "data" : {}
}

What we need is for each object returned to be serialized normally and be put under data in the hierarchy. Let's say we have this OperationContract:

ObjectToBeReturned test();

and ObjectToBeReturned is:

[DataContract]
class ObjectToBeReturned {
    [DataMember]
    public string A {get; set;}
    [DataMember]
    public string B {get; set;}
}

Now, we would like through TCP to exchange directly the ObjectToBeReturned object but through WebHttp to have the following format as a response:

{
    "status": "success",
    "data": {
        "A": "atest",
        "B": "btest"
    }
}

Possibility 1

We have considered two possibilities. The first one would be to have an object say named Response that would be the return object of all our OperationContract and it would contain the following:

[DataContract]
class Response<T> {
    [DataMember]
    public string Status {get; set;}
    [DataMember]
    public T Data {get; set;}
}

The problem with that is we would be required to exchange this object also through the TCP protocol but that is not our ideal scenario.

Possibility 2

We have tried to add a custom EndpointBehavior with a custom IDispatchMessageFormatter that would only be present for the WebHttp endpoint.

In this class, we implemented the following method:

 public Message SerializeReply(
            MessageVersion messageVersion,
            object[] parameters,
            object result)
        {

            var clientAcceptType = WebOperationContext.Current.IncomingRequest.Accept;

            Type type = result.GetType();

            var genericResponseType = typeof(Response<>);
            var specificResponseType = genericResponseType.MakeGenericType(result.GetType());
            var response = Activator.CreateInstance(specificResponseType, result);

            Message message;
            WebBodyFormatMessageProperty webBodyFormatMessageProperty;


            if (clientAcceptType == "application/json")
            {
                message = Message.CreateMessage(messageVersion, "", response, new DataContractJsonSerializer(specificResponseType));
                webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);

            }
            else
            {
                message = Message.CreateMessage(messageVersion, "", response, new DataContractSerializer(specificResponseType));
                webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Xml);

            }

            var responseMessageProperty = new HttpResponseMessageProperty
            {
                StatusCode = System.Net.HttpStatusCode.OK
            };

            message.Properties.Add(HttpResponseMessageProperty.Name, responseMessageProperty);

            message.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty); 
            return message;
        }

This seems really promising. The problem with that method is that when serializing with the DataContractSerializer we get the following error:

Consider using a DataContractResolver if you are using DataContractSerializer or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to the serializer.

We really don't want to list all of our known types above our Response class since there will be too many and maintenance will be a nightmare (when we listed the knowntypes we were able to get the data). Please note that all the objects passed to response will be decorated with a DataContract attribute.

I have to mention that we don't care if changing the message format can cause the WebHttp endpoint to not be accessible via a ServiceReference in another C# project, they should use TCP for that.

Questions

Basically, we only want to customize the return format for WebHttp so, the questions are:

  • Is there an easier way to accomplish that than what we are doing?
  • Is there a way to tell the serializer the knowntype from the result parameter's type in the SerializeReply method?
  • Should we implement a custom Serializer that will be called in the MessageDispatcherFormatter that will tweak the format to fit ours ?

We feel we are on the right path but some parts are missing.


Solution

  • Here's how I was able to accomplish what I wanted. May not be the perfect solution but is working well in my case.

    Possibility #2 was the way to go. But I had to change it to this:

     public Message SerializeReply(
                MessageVersion messageVersion,
                object[] parameters,
                object result)
            {
                // In this sample we defined our operations as OneWay, therefore, this method
                // will not get invoked.
    
    
    
                var clientAcceptType = WebOperationContext.Current.IncomingRequest.Accept;
    
                Type type = result.GetType();
    
                var genericResponseType = typeof(Response<>);
                var specificResponseType = genericResponseType.MakeGenericType(result.GetType());
                var response = Activator.CreateInstance(specificResponseType, result);
    
                Message message;
                WebBodyFormatMessageProperty webBodyFormatMessageProperty;
    
    
                if (clientAcceptType == "application/json")
                {
                    message = Message.CreateMessage(messageVersion, "", response, new DataContractJsonSerializer(specificResponseType));
                    webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Json);
    
                }
                else
                {
                    message = Message.CreateMessage(messageVersion, "", response, new DataContractSerializer(specificResponseType));
                    webBodyFormatMessageProperty = new WebBodyFormatMessageProperty(WebContentFormat.Xml);
    
                }
    
                var responseMessageProperty = new HttpResponseMessageProperty
                {
                    StatusCode = System.Net.HttpStatusCode.OK
                };
    
                message.Properties.Add(HttpResponseMessageProperty.Name, responseMessageProperty);
    
                message.Properties.Add(WebBodyFormatMessageProperty.Name, webBodyFormatMessageProperty); 
                return message;
            }
    

    The key here was that since Response is a generic Type, WCF needs to know all the known types and listing them by hand wasn't a possibility. I decided that all my return types would implement a custom IDataContract class (yes, empty):

    public interface IDataContract
    {
    
    }
    

    Then, what I did in Response, is to implement aGetKnownTypes method and in it, loop through all the classes implementing the IDataContract in the Assembly and return them in an Array. Here's my Response object:

    [DataContract(Name = "ResponseOf{0}")]
        [KnownType("GetKnownTypes")]
        public class Response<T>
            where T : class
        {
    
            public static Type[] GetKnownTypes()
            {
                var type = typeof(IDataContract);
                var types = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(s => s.GetTypes())
                    .Where(p => type.IsAssignableFrom(p));
    
                return types.ToArray();
            }
    
            [DataMember(Name = "status")]
            public ResponseStatus ResponseStatus { get; set; }
    
            [DataMember(Name = "data")]
            public object Data { get; set; }
    
            public Response()
            {
                ResponseStatus = ResponseStatus.Success;
            }
    
            public Response(T data) : base()
            {
                Data = data;            
            }
        }
    

    This allowed me to connect through TCP and exchange objects directly and have a great serialization via WebHTTP in either JSON or XML.