8

From this blog post, I was able to create a custom WCF IDispatchMessageFormatter that uses JSON.NET serialization. It works great with one caveat: using it with UriTemplate doesn't necessarily work as expected.

Here's the implementation provided by the blog post:

class NewtonsoftJsonDispatchFormatter : IDispatchMessageFormatter
{
    private readonly OperationDescription od;
    private readonly ServiceEndpoint ep;
    private readonly Dictionary<string, int> parameterNames = new Dictionary<string, int>();

    public NewtonsoftJsonDispatchFormatter(OperationDescription od, ServiceEndpoint ep, bool isRequest)
    {
        this.od = od;
        this.ep = ep;
        if (isRequest)
        {
            int operationParameterCount = od.Messages[0].Body.Parts.Count;
            if (operationParameterCount > 1)
            {
                this.parameterNames = new Dictionary<string, int>();
                for (int i = 0; i < operationParameterCount; i++)
                {
                    this.parameterNames.Add(od.Messages[0].Body.Parts[i].Name, i);
                }
            }
        }
    }
    public void DeserializeRequest(Message message, object[] parameters)
    {
        if (message.IsEmpty) 
            return;

        object bodyFormatProperty;

        if (!message.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out bodyFormatProperty) ||
            (bodyFormatProperty as WebBodyFormatMessageProperty).Format != WebContentFormat.Raw)
        {
            throw new InvalidOperationException("Incoming messages must have a body format of Raw. Is a ContentTypeMapper set on the WebHttpBinding?");
        }

        XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();
        bodyReader.ReadStartElement("Binary");
        byte[] rawBody = bodyReader.ReadContentAsBase64();

        using (MemoryStream ms = new MemoryStream(rawBody))
        using (StreamReader sr = new StreamReader(ms))
        {
            if (parameters.Length == 1)
                parameters[0] = Helper.serializer.Deserialize(sr, od.Messages[0].Body.Parts[0].Type);
            else
            {
                // multiple parameter, needs to be wrapped
                using (Newtonsoft.Json.JsonReader reader = new Newtonsoft.Json.JsonTextReader(sr))
                {
                    reader.Read();
                    if (reader.TokenType != Newtonsoft.Json.JsonToken.StartObject)
                        throw new InvalidOperationException("Input needs to be wrapped in an object");
                    reader.Read();
                    while (reader.TokenType == Newtonsoft.Json.JsonToken.PropertyName)
                    {
                        string parameterName = reader.Value as string;
                        reader.Read();
                        if (this.parameterNames.ContainsKey(parameterName))
                        {
                            int parameterIndex = this.parameterNames[parameterName];
                            parameters[parameterIndex] = Helper.serializer.Deserialize(reader, this.od.Messages[0].Body.Parts[parameterIndex].Type);
                        }
                        else
                            reader.Skip();
                        reader.Read();
                    }
                }
            }
        }
    }

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

Basically, the object[] parameters in the DeserializeMethod signature are out parameters that this method needs to instantiate.

So, this does a great job to handle a REST endpoint like this:

[WebInvoke(Method="POST", UriTemplate="foo/")]
public Foo MakeFoo(Foo foo) { ... }

or like this:

[WebInvoke(Method="POST", UriTemplate="FooBar/")]
public FooBar FooBar(Foo foo, Bar bar) { .. }

but it currently doesn't map the URI template parameters to the method parameters, e.g. something like this:

[WebGet(UriTemplate="Foo/{id}")]
public Foo GetFoo(string id) { ... }

Microsoft writes on the overriden GetRequestDispatchFormatter:

This is an extensibility point that derived behaviors can use to supply their own implementation of IDispatchMessageFormatter that is called to deserialize the input parameters of the service operation from the request message. Parameters specified in the UriTemplate of the service operation must be deserialized from the To URI of the request message and other parameters must be deserialized from the body of the request message.

So, great. I updated the deserialization of the parameters from the body of a message. But I don't want to override deserializing the parameters in the UriTemplate. Is there any way to use existing code to map the incoming URI request to the parameters with the default way UriTemplate is handled?

It seems I need to use something like the UriTemplateDispatchFormatter but I'm not sure how to implement this, and it's non-public.

Joseph Nields
  • 5,527
  • 2
  • 32
  • 48
  • I'm from the future every link went down. How did you register `NewtonsoftJsonDispatchFormatter` on the server? I fail to find the WebConfig part. – Drag and Drop Sep 30 '20 at 09:28

2 Answers2

8

Well, this is maybe the most ridiculous thing I've had to do, but copying the source code for UriTemplateDispatchFormatter, you can simply return a UriTemplateDispatchFormatter with an "inner" IDispatchFormatter that corresponds to the IDispatchFormatter I provided here. Not sure why this class was made internal >_>

the following class definition:

class UriTemplateDispatchFormatter : IDispatchMessageFormatter
{
    internal Dictionary<int, string> pathMapping;
    internal Dictionary<int, KeyValuePair<string, Type>> queryMapping;
    Uri baseAddress;
    IDispatchMessageFormatter bodyFormatter;
    string operationName;
    QueryStringConverter qsc;
    int totalNumUTVars;
    UriTemplate uriTemplate;

    public UriTemplateDispatchFormatter(OperationDescription operationDescription, IDispatchMessageFormatter bodyFormatter, QueryStringConverter qsc, string contractName, Uri baseAddress)
    {
        this.bodyFormatter = bodyFormatter;
        this.qsc = qsc;
        this.baseAddress = baseAddress;
        this.operationName = operationDescription.Name;
        Populate(
            out this.pathMapping,
            out this.queryMapping,
            out this.totalNumUTVars,
            out this.uriTemplate,
            operationDescription,
            qsc,
            contractName);
    }

    public void DeserializeRequest(Message message, object[] parameters)
    {
        object[] bodyParameters = new object[parameters.Length - this.totalNumUTVars];

        if (bodyParameters.Length != 0)
        {
            this.bodyFormatter.DeserializeRequest(message, bodyParameters);
        }
        int j = 0;
        UriTemplateMatch utmr = null;
        string UTMRName = "UriTemplateMatchResults";
        if (message.Properties.ContainsKey(UTMRName))
        {
            utmr = message.Properties[UTMRName] as UriTemplateMatch;
        }
        else
        {
            if (message.Headers.To != null && message.Headers.To.IsAbsoluteUri)
            {
                utmr = this.uriTemplate.Match(this.baseAddress, message.Headers.To);
            }
        }
        NameValueCollection nvc = (utmr == null) ? new NameValueCollection() : utmr.BoundVariables;
        for (int i = 0; i < parameters.Length; ++i)
        {
            if (this.pathMapping.ContainsKey(i) && utmr != null)
            {
                parameters[i] = nvc[this.pathMapping[i]];
            }
            else if (this.queryMapping.ContainsKey(i) && utmr != null)
            {
                string queryVal = nvc[this.queryMapping[i].Key];
                parameters[i] = this.qsc.ConvertStringToValue(queryVal, this.queryMapping[i].Value);
            }
            else
            {
                parameters[i] = bodyParameters[j];
                ++j;
            }
        }
    }


    public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
    {
        throw new NotImplementedException();
    }

    private static void Populate(out Dictionary<int, string> pathMapping,
    out Dictionary<int, KeyValuePair<string, Type>> queryMapping,
    out int totalNumUTVars,
    out UriTemplate uriTemplate,
    OperationDescription operationDescription,
    QueryStringConverter qsc,
    string contractName)
    {
        pathMapping = new Dictionary<int, string>();
        queryMapping = new Dictionary<int, KeyValuePair<string, Type>>();
        string utString = GetUTStringOrDefault(operationDescription);
        uriTemplate = new UriTemplate(utString);
        List<string> neededPathVars = new List<string>(uriTemplate.PathSegmentVariableNames);
        List<string> neededQueryVars = new List<string>(uriTemplate.QueryValueVariableNames);
        Dictionary<string, byte> alreadyGotVars = new Dictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
        totalNumUTVars = neededPathVars.Count + neededQueryVars.Count;
        for (int i = 0; i < operationDescription.Messages[0].Body.Parts.Count; ++i)
        {
            MessagePartDescription mpd = operationDescription.Messages[0].Body.Parts[i];
            string parameterName = XmlConvert.DecodeName(mpd.Name);
            if (alreadyGotVars.ContainsKey(parameterName))
            {
                throw new InvalidOperationException();
            }
            List<string> neededPathCopy = new List<string>(neededPathVars);
            foreach (string pathVar in neededPathCopy)
            {
                if (string.Compare(parameterName, pathVar, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    if (mpd.Type != typeof(string))
                    {
                        throw new InvalidOperationException();
                    }
                    pathMapping.Add(i, parameterName);
                    alreadyGotVars.Add(parameterName, 0);
                    neededPathVars.Remove(pathVar);
                }
            }
            List<string> neededQueryCopy = new List<string>(neededQueryVars);
            foreach (string queryVar in neededQueryCopy)
            {
                if (string.Compare(parameterName, queryVar, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    if (!qsc.CanConvert(mpd.Type))
                    {
                        throw new InvalidOperationException();
                    }
                    queryMapping.Add(i, new KeyValuePair<string, Type>(parameterName, mpd.Type));
                    alreadyGotVars.Add(parameterName, 0);
                    neededQueryVars.Remove(queryVar);
                }
            }
        }
        if (neededPathVars.Count != 0)
        {
            throw new InvalidOperationException();
        }
        if (neededQueryVars.Count != 0)
        {
            throw new InvalidOperationException();
        }
    }
    private static string GetUTStringOrDefault(OperationDescription operationDescription)
    {
        string utString = GetWebUriTemplate(operationDescription);
        if (utString == null && GetWebMethod(operationDescription) == "GET")
        {
            utString = MakeDefaultGetUTString(operationDescription);
        }
        if (utString == null)
        {
            utString = operationDescription.Name;
        }
        return utString;
    }
    private static string MakeDefaultGetUTString(OperationDescription od)
    {
        StringBuilder sb = new StringBuilder(XmlConvert.DecodeName(od.Name));
        //sb.Append("/*"); // note: not + "/*", see 8988 and 9653
        if (!IsUntypedMessage(od.Messages[0]))
        {
            sb.Append("?");
            foreach (MessagePartDescription mpd in od.Messages[0].Body.Parts)
            {
                string parameterName = XmlConvert.DecodeName(mpd.Name);
                sb.Append(parameterName);
                sb.Append("={");
                sb.Append(parameterName);
                sb.Append("}&");
            }
            sb.Remove(sb.Length - 1, 1);
        }
        return sb.ToString();
    }
    private static bool IsUntypedMessage(MessageDescription message)
    {

        if (message == null)
        {
            return false;
        }
        return (message.Body.ReturnValue != null && message.Body.Parts.Count == 0 && message.Body.ReturnValue.Type == typeof(Message)) ||
            (message.Body.ReturnValue == null && message.Body.Parts.Count == 1 && message.Body.Parts[0].Type == typeof(Message));
    }
    private static void EnsureOk(WebGetAttribute wga, WebInvokeAttribute wia, OperationDescription od)
    {
        if (wga != null && wia != null)
        {
            throw new InvalidOperationException();
        }
    }
    private static string GetWebUriTemplate(OperationDescription od)
    {
        // return exactly what is on the attribute
        WebGetAttribute wga = od.Behaviors.Find<WebGetAttribute>();
        WebInvokeAttribute wia = od.Behaviors.Find<WebInvokeAttribute>();
        EnsureOk(wga, wia, od);
        if (wga != null)
        {
            return wga.UriTemplate;
        }
        else if (wia != null)
        {
            return wia.UriTemplate;
        }
        else
        {
            return null;
        }
    }
    private static string GetWebMethod(OperationDescription od)
    {
        WebGetAttribute wga = od.Behaviors.Find<WebGetAttribute>();
        WebInvokeAttribute wia = od.Behaviors.Find<WebInvokeAttribute>();
        EnsureOk(wga, wia, od);
        if (wga != null)
        {
            return "GET";
        }
        else if (wia != null)
        {
            return wia.Method ?? "POST";
        }
        else
        {
            return "POST";
        }
    }

}

along with the following behavior:

class NewtonsoftJsonBehavior : WebHttpBehavior
{
    protected override IDispatchMessageFormatter GetRequestDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
    {
        return new UriTemplateDispatchFormatter(
            operationDescription,
            new NewtonsoftJsonDispatchFormatter(operationDescription, endpoint, true),
            GetQueryStringConverter(operationDescription),
            endpoint.Contract.Name,
            endpoint.Address.Uri);
    }

    protected override IDispatchMessageFormatter GetReplyDispatchFormatter(OperationDescription od, ServiceEndpoint ep)
    {
        return new NewtonsoftJsonDispatchFormatter(od, ep, false);
    }

}

works

Joseph Nields
  • 5,527
  • 2
  • 32
  • 48
  • 1
    very pragmatic approach! One thing to keep in mind is that @carlosfigueira's NewtonsoftJsonDispatchFormatter implementation expects the first body part to be the payload body. If the method is structured such that the body part is not the first argument it will likely result in a `JsonReaderException`. – Tedford Jun 02 '16 at 13:10
  • Hi, I ended up having the same issue, is it still the best solution available ? I'm giving it a try but I am not fan of copying code from the framework in my own solution but oh well ... – vinhent Mar 13 '17 at 22:29
  • 1
    @Vinhent the better solution might be to use something other than WCF for a RESTful web service, but if you need to stick to WCF yes it is – Joseph Nields Mar 14 '17 at 21:42
0

Joseph Nields's answer is perfect; I just would like to share a slight different approach, using reflection to get an UriTemplateDispatchFormatter's instance, without having to replicate its code:

protected override IDispatchMessageFormatter GetRequestDispatchFormatter(
    OperationDescription operationDescription, ServiceEndpoint endpoint) =>

    (IDispatchMessageFormatter)Activator.CreateInstance(
        Type.GetType("System.ServiceModel.Dispatcher.UriTemplateDispatchFormatter, " +
            "System.ServiceModel.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"),
        operationDescription,
        new NewtonsoftJsonDispatchFormatter(operationDescription, endpoint, true),
        GetQueryStringConverter(operationDescription),
        endpoint.Contract.Name,
        endpoint.Address.Uri
    );
Daniel Brilho
  • 190
  • 2
  • 5