RESTful Web Conversations 3
This is the final post in the series concerning my RESTful conversation framework /part 1/part 2/. This post describes my cleaning up of some loose ends in the system, and finally shows a production sample.
If you cannot be bothered to read all of this, just go and see the demo, available at http://nesteruk.org/talk. Source code is, as always, available at https://bitbucket.org/nesteruk/restfulwebconversation.
Back to JSONP
So the first things I wanted to do is to get JSONP back. As it turns out, the Web API still doesn’t have this built-in (why, I wonder?), but the project site was helpful – I found this post, which basically gave me an implementation of JSONP on a silver platter. Or not.
In actual fact, things went wrong from the start because
-
There’s still no support for the header which accepts any media type (i.e.,
Accept: */*) -
The default formatters ‘take over’ whatever you do
-
The implementation I found on the CodePlex site is just plain wrong
So let’s deal with the above issues. It’s clear that we have to define our own JsonpFormatter. But how do we handle the */* header? The answer is that we specify a media range mapping such that an unspecified range maps onto application/json:
public class JsonPFormatter : MediaTypeFormatter
{
public JsonPFormatter()
{
this.AddMediaRangeMapping(new MediaTypeHeaderValue("*/*"), new MediaTypeHeaderValue("application/json"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
}
...
}
The above basically ensures that:
-
A request with
*/*in the header will return JSON -
A request with an accept value other than
*/*orapplication/jsonwill be processed by another formatter. This ensures that XML support is still there.
Now, let’s finish out the formatter. First, we determine whther we can read or write a particular type. Since there is only one type in our app, this is somewhat easy:
protected override bool OnCanWriteType(Type type)
{
return type == typeof (ConversationItemDTO);
}
protected override bool OnCanReadType(Type type)
{
return false;
}
Now, we need to take care of the method that writes the resulting data to the stream. The original CodePlex sample made the mistake in that it assumed that a JSON request always had to be JSONP, but that’s not correct. So, I changed it to selectively return JSON or padded JSON depending on whether the callback parameter was provided:
...
var callback = queryString["callback"];
if (callback != null)
sb.Append(callback + "(" + jsonString + ");");
else
sb.Append(jsonString);
byte[] buffer = Encoding.UTF8.GetBytes(sb.ToString());
stream.Write(buffer, 0, buffer.Length);
So, given this, I hit a final problem: how to make sure that my formatter takes precedence over the default JSON formatter? After some searching, I found the solution: inserting it as the first formatter in the configuration:
// jsonp formatter comes first! config.Configuration.OperationHandlerFactory.Formatters.Insert( 0, container.Resolve<JsonPFormatter>());
And that’s it – I got my JSONP support! Consumption can now go back to normal:
JQuery.GetJSON(serverUrl + id + "?enabledOptions=" + !enabledOptions + "&callback=?", ...
Better conversation definitions
Admittedly, the current way of defining conversation lines with its enable/disable list is a bit archaic. Actually, it does serve its intended function, it’s just not very convenient – especially if I want to define a rather large conversation without using a graphical DSL.
My approach to this problem was to create a factory class that would take care of routine problems and would keep track of all the items for subsequent insertion. What I ended up is the following:
public class ConversationFactory
{
private int id = 1;
public List<ConversationItem> Items;
public ConversationFactory()
{
Items = new List<ConversationItem>();
}
public ConversationItem Create(Party party, string speech, List<int> disableList = null,
params ConversationItem[] enabledItems)
{
var ci = new ConversationItem();
ci.Id = id++;
ci.Speech = speech;
ci.PartyId = (int) party;
ci.DisableList = disableList ?? new List<int>();
ci.DisableList.Add(ci.Id);
ci.EnableList = new List<int>();
foreach (var i in enabledItems)
ci.EnableList.Add(i.Id);
Items.Add(ci);
return ci;
}
}
The above code generates the IDs and takes care of some of the obvious things, such as conversation items disabling themselves as soon as they are fired. Populating the site is then as easy as the following code:
var f = new ConversationFactory(); f.Create(Party.Server, "Welcome to my site! Feel free to ask questions.", null, f.Create(Party.Client, "Tell me about your professional life"), f.Create(Party.Client, "Tell me about your personal life"), f.Create(Party.Client, "Tell me about how this site works"), f.Create(Party.Client, "Actually, I think I better go now") ); repository.Upsert(f.Items);
The above set-up totally rocks for creating a simple state machine. Why? Because statements can be nested, meaning I can alternate server/client dialogue items for as long as I like.
Deployment
Deployment for the WCF REST service follows the typical protocols: you make a web application in the folder where the service is located, ensure it’s .Net 4, and specify wildcard mappings (yes, poor me, I’m on IIS 6). Subsequently updating the service is easy – you can just do ‘xcopy’ deployment via FTP. No restarts are necessary because generally IIS is smart enough to pick up changes on its own.
The great thing is that deployment of the client side is a breeze because it’s pure HTML. So you just copy over the files and everything works, though I’ve had to specify the default document (it’s an .html file, after all). Since the client side is totally detached, you can actually use your browser to debug those JSONP calls (I found an error this way), as opposed to the tedium of having to go to the server via RDP or something. This is in sharp contrast with the WCF service itself.
Conclusion & Futures
This series of posts has demonstrated some ways of working with a pure client-side web app that uses a REST backend. It is by no means complete, since I neglected to cover some of the important issues such as caching. However, it is my hope that this project serves as a reference application that one can build upon. ▪