3

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.

1
  • KnownTypeAttribute can also be used with a method that returns all known types instead of a static list. Perhaps this is an option for you if you can collect all necessary types (for example via reflection) at runtime. See: msdn.microsoft.com/en-us/library/ms584302%28v=vs.110%29.aspx Commented Apr 28, 2016 at 14:59

2 Answers 2

1

You're almost in the right track - having an endpoint behavior that applies only to the JSON endpoint is surely the way to go. You can, however, use a message inspector, which is a little simpler that a formatter. On the inspector, you can take the existing response, if it is a JSON response, and wrap the content with your wrapping object.

Notice that the WCF innards are all XML-based, so you will need to use the Mapping Between JSON and XML, but that's not too complicated.

The code below shows an implementation for this scenario.

public class StackOverflow_36918281
{
    [DataContract] public class ObjectToBeReturned
    {
        [DataMember]
        public string A { get; set; }
        [DataMember]
        public string B { get; set; }
    }
    [ServiceContract]
    public interface ITest
    {
        [OperationContract, WebGet(ResponseFormat = WebMessageFormat.Json)]
        ObjectToBeReturned Test();
    }
    public class Service : ITest
    {
        public ObjectToBeReturned Test()
        {
            return new ObjectToBeReturned { A = "atest", B = "btest" };
        }
    }
    public class MyJsonWrapperInspector : IEndpointBehavior, IDispatchMessageInspector
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
        }

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            return null;
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            object propValue;
            if (reply.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out propValue) &&
                ((WebBodyFormatMessageProperty)propValue).Format == WebContentFormat.Json)
            {
                XmlDocument doc = new XmlDocument();
                doc.Load(reply.GetReaderAtBodyContents());
                var newRoot = doc.CreateElement("root");
                SetTypeAttribute(doc, newRoot, "object");

                var status = doc.CreateElement("status");
                SetTypeAttribute(doc, status, "string");
                status.AppendChild(doc.CreateTextNode("success"));
                newRoot.AppendChild(status);

                var newData = doc.CreateElement("data");
                SetTypeAttribute(doc, newData, "object");
                newRoot.AppendChild(newData);

                var data = doc.DocumentElement;
                var toCopy = new List<XmlNode>();
                foreach (XmlNode child in data.ChildNodes)
                {
                    toCopy.Add(child);
                }

                foreach (var child in toCopy)
                {
                    newData.AppendChild(child);
                }

                Console.WriteLine(newRoot.OuterXml);

                var newReply = Message.CreateMessage(reply.Version, reply.Headers.Action, new XmlNodeReader(newRoot));
                foreach (var propName in reply.Properties.Keys)
                {
                    newReply.Properties.Add(propName, reply.Properties[propName]);
                }

                reply = newReply;
            }
        }

        private void SetTypeAttribute(XmlDocument doc, XmlElement element, string value)
        {
            var attr = element.Attributes["type"];
            if (attr == null)
            {
                attr = doc.CreateAttribute("type");
                attr.Value = value;
                element.Attributes.Append(attr);
            }
            else
            {
                attr.Value = value;
            }
        }

        public void Validate(ServiceEndpoint endpoint)
        {
        }
    }
    public static void Test()
    {
        string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
        string baseAddressTcp = "net.tcp://" + Environment.MachineName + ":8888/Service";
        ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress), new Uri(baseAddressTcp));
        var ep1 = host.AddServiceEndpoint(typeof(ITest), new NetTcpBinding(), "");
        var ep2 = host.AddServiceEndpoint(typeof(ITest), new WebHttpBinding(), "");
        ep2.EndpointBehaviors.Add(new WebHttpBehavior());
        ep2.EndpointBehaviors.Add(new MyJsonWrapperInspector());
        host.Open();
        Console.WriteLine("Host opened");

        Console.WriteLine("TCP:");
        ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new NetTcpBinding(), new EndpointAddress(baseAddressTcp));
        ITest proxy = factory.CreateChannel();
        Console.WriteLine(proxy.Test());
        ((IClientChannel)proxy).Close();
        factory.Close();


        Console.WriteLine();
        Console.WriteLine("Web:");
        WebClient c = new WebClient();
        Console.WriteLine(c.DownloadString(baseAddress + "/Test"));

        Console.Write("Press ENTER to close the host");
        Console.ReadLine();
        host.Close();
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.