.NET Zone is brought to you in partnership with:

Joel Cochran is a Senior Technology Consultant with Lotic Factor Inc, of Roanoke, VA, a business and technology consulting firm. He is a Microsoft Expression Blend MVP, an ASPInsider, and is currently serving as the INETA Mentor for Virginia. Joel is the author of "Expression Blend in Action" by Manning Publications and "The Practical MVVM Manifesto" (http://practicalmvvm.com). A technology generalist, he works with technologies such as Windows 8, WPF, ASP.NET MVC4, SQL Server, SSIS, and Azure. His languages of choice are C# and JavaScript. He spends an inordinate amount of time on Twitter (@joelcochran) and far less time on his blog http://joelcochran.com. Joel is a DZone MVB and is not an employee of DZone and has posted 4 posts at DZone. You can read more from them at their website. View Full User Profile

CORS, AJAX, and POST data in Web API

11.17.2012
| 7757 views |
  • submit to reddit

Last week was quite possibly the most frustrating week of my 15+ 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.

Here is the completed version of our CorsAjax JavaScript file. Just drop this script into your page and it will automatically create a global variable called “corsAjax” which you can use to issue GET and POST requests.

(function (window) {
  function CorsAjax() {
    this.post = function(url, data, callback) {
      $.support.cors = true;
      var jqxhr = $.post(url, data, callback, "json")
             .error(function(jqXhHR, status, errorThrown) {
               if ($.browser.msie && window.XDomainRequest) {
                   var xdr = new XDomainRequest();
                   xdr.open("post", url);
                   xdr.onload = function () {
                    if (callback) {
                     callback(
                      JSON.parse(this.responseText), 
                      'success');
                    }
                  };
                  xdr.send(data);
               } else {
                 alert("corsAjax.post error: " + status + ", " + errorThrown);
               }
      });
    };

    this.get = function(url, callback) {
        $.support.cors = true;
        var jqxhr = $.get(url, null, callback, "json")
               .error(function(jqXhHR, status, errorThrown) {
                  if ($.browser.msie && window.XDomainRequest) {
                    var xdr = new XDomainRequest();
                    xdr.open("get", url);
                    xdr.onload = function () {
                     if (callback) {
                      callback(
                       JSON.parse(this.responseText), 
                       'success');
                     }
                    };
                    xdr.send();
                  } else {
                    alert("corsAjax.get error: " + status + ", " + errorThrown);
                  }
                });
        };
    };

    window.corsAjax = new CorsAjax();
})(window);

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:

$("#btn").on("click", function postData() {
    var data =
        {
            Id: 5,
            Name: "Joel",
            BoolValue: true
        };
    corsAjax.post(apiUrl + "/Api/Values/MyAction", 
                               data);
});

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:

$("#btn").on("click", function postData() {
    var data =
        {
            Id: 5,
            Name: "Joel",
            BoolValue: true
        };
    corsAjax.post(apiUrl + "/Api/Values/MyAction",
             ko.toJSON(data, null, 0));
});

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:

string jsonString = HttpContext.Current.Request.Form.GetValues(0)[0].ToString();

Next you will need to deserialize the JSON into your object:

var js = new Newtonsoft.Json.JsonSerializer();
result = js.Deserialize<JsonData>(new Newtonsoft.Json.JsonTextReader(
               new System.IO.StringReader(jsonString)));

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:

public class GenericContextDeserializer<T> where T : new()
{
    public T DeserializeToType(HttpContext context)
    {
        T result = new T();
        if (context != null)
        {
            try
            {
                string jsonString = context.Request.Form.GetValues(0)[0].ToString();
                Newtonsoft.Json.JsonSerializer js = new Newtonsoft.Json.JsonSerializer();
                result = js.Deserialize<T>(new Newtonsoft.Json.JsonTextReader(
                               new System.IO.StringReader(jsonString)));
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        else
            throw new NullReferenceException();
        return result;
    }
}

Using the GenericDeserializer, here is the completed Action to deserialize POST data:

 

[HttpPost]
[HttpOptions]
public HttpResponseMessage MyAction(JsonData data)
{
    var results = Request.CreateResponse();
    try
    {
        data = new GenericContextDeserializer<JsonData>()                   .DeserializeToType(HttpContext.Current);
        // do stuff with data
        results.StatusCode = HttpStatusCode.OK;
    }
    catch (Exception ex)
    {
        results.StatusCode = HttpStatusCode.InternalServerError;
    }
    return results;
}

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:

public static class WebContextExtensions
{
    public static bool IsDataSerialized(this HttpContext context)
    {
        return context.Request.Params.AllKeys[0] == null;
    }
}

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:

[HttpPost]
[HttpOptions]
public HttpResponseMessage MyAction(JsonData data)
{
    var results = Request.CreateResponse();
    try
    {
        if (HttpContext.Current.IsDataSerialized())
        {

            data = new GenericContextDeserializer<JsonData>()                                 .DeserializeToType(HttpContext.Current);
        }
        // do stuff with data
        results.StatusCode = HttpStatusCode.OK;
    }
    catch (Exception ex)
    {
        results.StatusCode = HttpStatusCode.InternalServerError;
    }
    return results;
}

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.

Published at DZone with permission of Joel Cochran, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)