How to Replace WCF Serialization with Json.Net without Wrapping and with UriTemplate Support

First, yes, I am still using WCF. Let’s move passed that concern to the real concern.

There are a dozen blog posts out there that explain how to replace the WCF serializer with Json.Net, however, every last one of them says that you must use wrapping and using parameters in the UriTemplate is not supported. https://blogs.msdn.microsoft.com/carlosfigueira/2011/05/02/wcf-extensibility-message-formatters

Just search the internet for WCF IDispatchMessageFormatter Json.Net. You will find all the articles that only work without UriTemplate support.

Well, I needed it to work with UriTemplate support without wrapping.

Turns out that this solution is far easier than I expected. I came accross this solution only after spending hours browsing Microsoft’s code.

So, to start, using parameters in the UriTemplate means that your Url or Url parameters will be specified in the UriTemplate and will have parameters.

For example, the Odata spec says that you should access an entity by Id with this a Url similar to this one:

https://somesite.tld/some/service/Users(1)

Then the method for the WCF service is like this:

[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "Users({{id}})", ResponseFormat = WebMessageFormat.Json)]
OdataObject Get(string id);
public virtual OdataObject Get(string id) 
{
    // code here
}

That is fine for a GET call as it doesn’t have a body. But what about a POST, Patch, or PUT call that does have a body? And what about now that the world is realizing that a GET sometimes needs a body?

Also, the examples provided a lot of code to figure out if it is a GET call and not even use the custom Json.Net IDispatchMessageFormatter. None of that code is necessary with this solution.

Let’s look at a PUT call that updates a single property of an entity as this has two parameters in the UriTemplate as well as a message body.

[OperationContract]
[WebInvoke(Method = "PUT", UriTemplate = "Users({{id}})/{{Property}} ResponseFormat = WebMessageFormat.Json)]
string UpdateProperty(string id, string property, string value);

public virtual OdataObject Put(string id, string property, string value)
{
// code here to update user
}

So there are two parameters in the UriTemplate, id and property, and the last parameter, value, is in the message body. Not a single solution for replacing the WCF serializer with Json.Net supports this scenario. Until now.

The goal is to deserialize the request with Json.Net. But the solutions provided break UriTemplate parameters in trying to reach the goal. The goal is not to replace the default WCF UriTemplate parameter work.

So now we can define a new problem: How do we deserialize the body with Json.Net but still have the UriTemplate parameters handled by WCF? The code to deserialize is the same code for both the parameters and the message body. We need to get the parameters without having WCF use the default deserializer for the message body.

Turns out, this problem is easy to solve.

Microsoft published their WCF code. Look at this code, lines 50-54: https://github.com/Microsoft/referencesource/blob/master/System.ServiceModel.Web/System/ServiceModel/Dispatcher/UriTemplateDispatchFormatter.cs

If you notice in line 50, WCF has the number of parameters from the Url and Url parameters and it subtracts that from the total list of parameters. If the message has not body, the subtraction result is always 0. If the message has a body, the subtraction always results in 1, telling WCF to deserialize the body. Well, I want WCF to do what it normally does with UriTempalte parameters, so if there is no body, use the WCF default stuff (which all the blogs say to do, but they do it the hard way).

Solution:

  1. In the custom EndPointBehavior, on the override, store the default IDispatchMessageFormater and pass it into the CustomDispatchMessageFormatter.
protected override IDispatchMessageFormatter GetReplyDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
{
    var parentFormatter = base.GetReplyDispatchFormatter(operationDescription, endpoint);
    return new CustomDispatchMessageFormatter(this, operationDescription, parentFormatter);
}
  1. If there is no body, use the WCF default DeserializeRequest method. This vastly simplifies the code on the blogs out there. The other examples had masses of code upstream that just wasn’t needed when message.IsEmpty could be used.
  2. If there is a body but no parameters, just use Json.Net.
  3. If there is a body and there are UriTemplate parameters, create a temparary parameter array 1 size smaller and pass that into the default serializer.
  4. Copy the temp array to the orignal array.
  5. Then just deserialize with Json.Net.
public void DeserializeRequest(Message message, object[] parameters)
{
     if (message.IsEmpty || parameters.Length == 0)
         ParentFormatter.DeserializeRequest(message, parameters);
     else
         DeserializeMessageWithBody(message, parameters);
} 

private void DeserializeMessageWithBody(Message message, object[] parameters)
{
     if (parameters.Length > 1)
     {
         object[] tmpParams = new object[parameters.Length - 1];
         ParentFormatter.DeserializeRequest(message, tmpParams);
         tmpParams.CopyTo(parameters, 0);
     }
     if (message.GetWebContentFormat() != WebContentFormat.Raw)
         throw new InvalidOperationException("Incoming messages must have a body format of Raw.");
     byte[] rawBody = message.GetRawBody();
         var type = OperationDescription.Messages[0].Body.Parts.Last().Type;
         parameters[parameters.Length - 1] = RawBodyDeserializer.Deserialize(rawBody, type);
}

The deserializer becomes vastly simplified now that it isn’t trying to also handling wrapped parameters.

public class RawBodyDeserializer : IRawBodyDeserializer
{
    public object Deserialize(byte[] rawBody, Type type)
    { 
        using (MemoryStream ms = new MemoryStream(rawBody))
        using (StreamReader sr = new StreamReader(ms))
        {
            JsonSerializer serializer = new JsonSerializer();
            return serializer.Deserialize(sr, type);
        }
    }
}

Leave a Reply

How to post code in comments?