Thursday 18 August 2011

JSONP FTW

We’ve been looking at how to integrate our stuff with MS Dynamics CRM. Dynamics does not play nice with others. I won’t go into the details here but I think we’re going to end up using javascript as a proxy to get things done. As a result I’ve been looking at the new (ish) WebAPI bits from the WCF team (http://wcf.codeplex.com). The basic motivation behind this project is to make WCF talk HTTP like a native. Things like content format negotiation are baked in. So you can write a single service and have different clients receive differently formatted responses. So a JQuery ajax call will see JSON another client might see XML. You can even point a browser at your service and get HTML via Razor templates (very useful if you want to add an admin UI to your service.
The point of this post is about content format negotiation and dealing with JsonP.
A lot (errr most) of this is taken from Alexander Zeitlers article. He mentions part way through to grab a file from the WebAPI project. I’ve added a little of my own flavour to this part.

WebAPI has the concept of MediaTypeFormatters. When a request comes in it will have an “accept” header which tells the server which media types the client can handle. A JQuery ajax request would send “application/json”, a browser would send “text/html”.
The accept header value is used to look up which formatter to use to format the response.
There are times, however, when you want to force the format. Testing via a browser is one. But more importantly when using JsonP the request has an accept header of “*/*”. In this case you always want the response in json.
In the ContactManager_Advanced project in the samples included in the codeplex project there is an example of a “MessageChannel” that inspects the uri and sets the accept header. I’ve customised this a little so that it also looks for a “format” parameter in the querystring. It also forces to json if the is a “callback” parameter in the querystring.
Lastly I’ve changed the fluent interface. It made little sense to me to have an extension method on HttpApplication.
Here’s the listing:
    public static class UriFormatExtensionMessageChannelExtensions
    {
        public static IHttpHostConfigurationBuilder AddUriFormatExtension(this IHttpHostConfigurationBuilder builder)
        {
            return builder.AddMessageHandlers(typeof(UriFormatExtensionMessageChannel));
        }
    }

    public class UriFormatExtensionMessageChannel : DelegatingChannel
    {
        public UriFormatExtensionMessageChannel(HttpMessageChannel handler) : base(handler) { }

        private static Dictionary<string, MediaTypeWithQualityHeaderValue> extensionMappings = new Dictionary<string, MediaTypeWithQualityHeaderValue>();

        public static FluentExtensionMappings SetUriExtensionMapping(string extension, string mediaType)
        {
            extensionMappings[extension] = new MediaTypeWithQualityHeaderValue(mediaType);
            return new FluentExtensionMappings();
        }

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (!TryGetLastSegmentFormat(request))
                TryGetQSFormat(request);

            return base.SendAsync(request, cancellationToken);
        }

        /// <summary>
        /// Try to get the format from the last segment of the Uri
        /// </summary>
        /// <example>http://example.com/product/1/json</example>
        /// <param name="request"></param>
        /// <returns>true if a format was found</returns>
        private static bool TryGetLastSegmentFormat(HttpRequestMessage request)
        {
            var segments = request.RequestUri.Segments;
            var lastSegment = segments.LastOrDefault();

            MediaTypeWithQualityHeaderValue mediaType;
            if (extensionMappings.TryGetValue(lastSegment, out mediaType))
            {
                var newUri = request.RequestUri.OriginalString.Replace("/" + lastSegment, "");
                request.RequestUri = new Uri(newUri, UriKind.Absolute);
                request.Headers.Accept.Clear();
                request.Headers.Accept.Add(mediaType);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Try to get the format from the query string of the Uri.
        /// If it's a JsonP callback then force to json
        /// </summary>
        /// <example>http://example.com/product/1?format=json</example>
        /// <param name="request"></param>
        /// <returns>true if a format was found</returns>
        private static bool TryGetQSFormat(HttpRequestMessage request)
        {
            var qsValues = HttpUtility.ParseQueryString(request.RequestUri.Query);
            var format = qsValues["format"];
            bool rebuildUri = false;
            if (!string.IsNullOrEmpty(format))
                rebuildUri = true;

            // if it's a JsonP callback then force to json
            if (!string.IsNullOrEmpty(qsValues["callback"]))
                format = "json";

            MediaTypeWithQualityHeaderValue mediaType;
            if (!string.IsNullOrEmpty(format) && extensionMappings.TryGetValue(format, out mediaType))
            {
                if (rebuildUri)
                {
                    var newUriBuilder = new UriBuilder(request.RequestUri);
                    qsValues.Remove("format");
                    newUriBuilder.Query = qsValues.ToString();
                    request.RequestUri = newUriBuilder.Uri;
                }

                request.Headers.Accept.Clear();
                request.Headers.Accept.Add(mediaType);
                return true;
            }

            return false;
        }

        protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public sealed class FluentExtensionMappings
        {
            public FluentExtensionMappings SetUriExtensionMapping(string extension, string mediaType)
            {
                extensionMappings[extension] = new MediaTypeWithQualityHeaderValue(mediaType);
                return this;
            }
        }
    }



So now your global.asax Application_Start will have a snippet like this.


UriFormatExtensionMessageChannel
  .SetUriExtensionMapping("xml", "application/xml")
  .SetUriExtensionMapping("json", "application/json")
  .SetUriExtensionMapping("png", "image/png")
  .SetUriExtensionMapping("odata", "application/atom+xml");

var config = HttpHostConfiguration.Create()
  .AddUriFormatExtension()
  .AddJsonpHandler();



The AddJsonpHandler line is a simple extension method to wrap Alex’s JsonpResponseHandler.


    public static class JsonpResponseHandlerExtensions
    {
        public static IHttpHostConfigurationBuilder AddJsonpHandler(this IHttpHostConfigurationBuilder builder)
        {
            return builder.AddResponseHandlers(c => c.Add(new JsonpResponseHandler()), (s, d) => true);            
        }
    }



Although this example is using IIS to host the service all of this is equally applicable to self hosted services.

4 comments:

Beat Wolf said...

hi, i might be stupid, but there seems to be no HttpHostConfiguration class anywhere. Where can i find the complete sourcecode of your example?

Andy Pook said...

Not stoopid at all...

HttpHostConfiguration _was_ part of the new WebAPI bits being developed by Glen Block and co at Microsoft. See their site at http://wcf.codeplex.com

Their bits are still at the preview stage, though it's being used in anger by many including my company, it is still changing.

They renamed HttpHostConfiguration recently. My current live code uses WebApiConfiguration. Channels where also renamed to Handlers and I think there were some changes to the method signatures. I'll try to update the post (which I won't be able to get to soon). So the summary is to use something like...

var config = new WebApiConfiguration();
config.MessageHandlers.Add(typeof(UriFormatExtensionMessageHandler));

Where UriFormatExtensionMessageHandler is the UriFormatExtensionMessageChannel refactored to the new DelegatingHandler.


Hope this brief note gives you enough to go on

Beat Wolf said...

thank you for your fast answer. There is another question i have. Half of your code is used to convert the mimetype */* into application/json? isn't it easier to define return type to json? like this:
[WebInvoke(ResponseFormat = WebMessageFormat.Json, UriTemplate = "", Method = "POST")]

then all that is needed is that JsonpResponseHandler to convert the json answer into json-p if needed.
So all that is needed is actually the equivalent of this line?
builder.AddResponseHandlers(c => c.Add(new JsonpResponseHandler()), (s, d) => true);

trying to figure out now how to achieve this.

Its really easy to get lost with all this..

Andy Pook said...

I agree, it is confusing. The style of the class api's they are using is difficult to comprehend. Also how all the pipeline fits together is a little hard to discover.

I didn't want to define the response format. I have several different types of clients json, jsonp, bson, xml ...
Also some of the clients aren't good at setting the right content types on their requests. The only clue they give me is that there is a callback in the query string. Unfortunately I don't have control of those clients and have to deal with their "idiosyncrasies". So my "solution" has belts and braces.