Last week was quite possibly the most frustrating week of my 15+ year software career. Stuff that was working stopped and stuff that should work didn’t. Hours of research on the Internet and miles of code trying anything and everything to get over this one small hurdle, but all to no avail. The good news, if you can call it that, is that I wasn’t alone. As my Grandmother always said “misery loves company”, so I shared the problem with my co-workers at Lotic Factor Inc, Brian Lanham and Chris Atienza, and we all suffered together. And then we suffered some more.
For 3 days we rode an emotional roller coaster, hopefully trying new solutions only to end in failure. I don’t see how Edison survived 2,000 attempts at creating the light bulb. By the end of day 2 I was thinking of inflicting bodily harm on somebody, and by the middle of day 3 I was ready for that person to be me. If I’ve painted a depressing picture for you, then as a writer I have done my job: I think this is the first time that a software problem has actually depressed me.
However, as evidenced by the fact that I have yet to do myself in, we did finally manage to put together a solution. I just hope it’s a long time before something so frustrating crosses our paths again.
CORS, AJAX, and Chrome
Brian already wrote about the issue in detail, so I’m not going to rehash the specifics. You should go read his post first.
Here are the highlights (my version):
- Accessing resources across domains (also known as CORS, or “Cross-Origin Resource Sharing”) requires the remote domain to grant permission to the requesting domain. This makes perfect sense.
- The server must be configured to acknowledge said permission. You can do this through the Response Header or server configuration. This also makes sense.
- Google Chrome ignores these settings. This sucks. Hard.
OK, the last point may be a little unfair. Chrome doesn’t ignore them so much as makes you do a magic dance and stand on three toes of your left foot under a full moon during a month beginning with a vowel before it will recognize them. Also, it may actually be web-kit causing the issue and not Chrome per se, but as Chrome is my browser of choice for development I’m going to aim all my angst at Google. On the plus side, had I not been using Chrome for development, we wouldn’t have discovered the issue any time soon. Better to suffer now than later I suppose.
The CORS AJAX Handlers
Brian mentioned the CORS AJAX code we wrote, the core of which came from Zoiner Tejada’s article on CORS with Azure. We also found lots of other help from many places, and as always I am grateful for the awesome software development community.
The problem of POST data
Getting past the dreaded origin issue was a major relief, but it led to another problem sending POST data to the Web API controller actions. These actions have object parameters, which should automatically map the incoming data. What we found is that depending on how you prepare the data for the POST submission, sometimes this will work and sometimes it won’t.
If you send JSON, the mapping should occur automatically:
This will result in the object being correctly initialized with the JSON data.
Another common approach is to serialize the data first. In the following example, we’re using KnockoutJS’s ko.toJSON method:
If you POST the data in this manner, jQuery will create a Form object, serialize the form, and submit it. When you do this, the parameter object in the Action method will be initialized to default values.
This means you will need to first extract the serialized string from the Request and re-serialize it as JSON:
Next you will need to deserialize the JSON into your object:
This is a little on the ugly side, so naturally we wrote a GenericDeserializer<T> class that takes an HttpContext instance and returns the deserialized object:
Using the GenericDeserializer, here is the completed Action to deserialize POST data:
How to tell if the data was serialized
The problem with the preceding approach is that the controller doesn’t inherently know whether the data was serialized or not. If you call the GenericDeserializer when there is no data in the HttpContext to deserialize, it will throw an exception. Rather than relying on try/catch I would prefer to only call the GenericDeserializer if necessary. To that end, I dug around in the Request object and discovered a useful little tidbit that we can use to solve this problem.
The solution is in the Request.Params.AllKeys property. When the data is passed as regular JSON, the sent parameters will be the first ones in this list. If, however, the data was serialized, the first item in the AllKeys list will be null. I wrapped this up in an Extension Method to make reuse more palatable:
Now I can decide when to try and deserialize based on this Extension Method. If the data does not need to be deserialized, it will fallback on the default parameter object.
Putting it all together
The final solution Action code includes the correct method attributes (see Brian’s post referenced previously), a call to determine if the data is serialized, and if needed call GenericDeserializer:
While there are several places this could be streamlined, like using the Request object instead of HttpContext, I can now implement a consistent way to handle POST data in Web API Action methods without the client implementation being aware of specifically how to send the data.