2012-11-28

MVC: Any object you want, but not a string

I often refer to MVC — the Model-View-Controller pattern for building websites — as "Magic, Voodoo, & Conjuring", because a lot of things just happen by what seems like magic. Moreover, when something goes wrong, it's very difficult to find exactly where it went wrong, because the process is happening without writing any code.

I had one such example of something going wrong today. It was only after I got another pair of eyes to confirm I wasn't crazy that I found a StackOverflow question that addressed my question.

We had a JavaScript method that took a method name, a key, and an object, and posted that to an AJAX handler on the server, more or less like so:

$.ajax({
    url: "/Home/" + methodName + "?key=" + keyId,
    contentType: 'application/json',
    data: $.toJSON(dataObject),
    success: function (result) { doSomething(); },
    type: "POST"
});

This JavaScript code block was part of a plug-in that we wrote to apply to many different forms on the site, that shared common functionality. Depending on the data being processed, the dataObject could be anything from a string to a full-blown object. The controller methods all had similar signatures:

public ActionResult ProcessStringData(string key, string newData);

public ActionResult ProcessDataEntity(string key, DataEntity newData);

When the AJAX call was made, the querystring parameter key was correctly mapped to the method parameter key, and the JSON object in the request payload was magically deserialized and mapped to the newData parameter.

The problem was (as you may have guessed from the StackOverflow question), while this worked great for the object, it was completely ineffective for the string — despite the fact that the data was in the request input stream, the value would always be "null".

This is why it was such a head-scratcher. If the data was a DataEntity object and we were calling the ProcessDataEntity method, the AJAX payload would simply be:

{ prop1: "value", prop2: "value2" }

Note that nowhere in that text is the name newData, and yet the MVC framework somehow managed to interpret it as a DataEntity object and pass it as the newData parameter.

Calling the ProcessStringData method with a string as data, however, resulted in this payload:

"data value"

So why wasn't it magically treating this as a string and assigning it to the correct parameter?

Maybe the JSON deserialization magic was throwing an unseen error. Using Fiddler, I tried submitting a payload of { "data value" }. This resulted in a very visible error response from the server about an invalid JSON object. Since "data value" did not return the error, the deserializer must've been relatively fine with it.

Curiously, if I changed the payload to:

{ newData: "data value" }

it did, in fact, work. However, I didn't want to have to wrap a simple string with a named parameter object to get it to work — it would be inconsistent with the object-processing methods that did not require this massaging. (Consistency makes for much more maintainable code. Try bringing up a new developer to speed and explaining that data is in a certain format, except in a list of "special cases" that you're lucky if you remember to mention, and you discover this truth quickly.)

Looking through StackOverflow, it seems that it is possible to remove the content type to process strings. Again, this would have required determining if I was calling a string-based or object-based method and setting the content type (because I actually did want the application/json type for objects). It just didn't "feel" right.

I ended up wrapping the payload in an array. For whatever reason, it is unable to take a string payload and map it to a string parameter, but it is perfectly fine mapping a payload of the form [ "value" ] to a parameter of type List<string>.

It's just one of those things that requires some coding around, but the hard part is trying to understand why. And because it's all handled by magic, voodoo, and conjuring (a.k.a. some method buried deep inside the framework), it's not in your working code, and therefore very difficult to track down.