Using jQuery AJAX to talk to ASP.NET ASMX web services

In this post I am going to show how to make async calls from JavaScript to a ASMX service endpoint hosted under IIS in a different domain i.e. cross domain. This post is the result of the work I did recently on this subject and found out after much trial and error what works and what doesn’t. The observations and results are far from perfect but should give a good idea to anyone wanting to do a similar interaction. In the next post, I am going to be doing the same interaction but with WCF services, the reason to do a 2 part series is to compare and contrast between these 2 kinds of services and perhaps see which gives more flexibility, is easier to work with and is more scalable.

So the scenario is that I have a HTML/ASPX client side which uses JS to render some data (sourced from a third party) into an HTML table and then this data is converted to JSON and is then sent to ASMX web service by jquery ajax to say, write to a text file on the server. I am going to skip the service creation part as that’s pretty basic.

1. ASMX web service: I create a web service called MyService.asmx and create a method called HelloWorld in it which looks like this:

[WebService(Namespace = "http://aman.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]    
//the attribute below is by default commented out, when calling the service from JS, uncomment it [System.Web.Script.Services.ScriptService]
public class MyService : System.Web.Services.WebService
{
    //both ScriptMethod and WebMethod attributes are needed to allow this operation to be called //by JS clients.
    [WebMethod]
    //if these params are not set, they default to XML and GET respectively
    [ScriptMethod(ResponseFormat = ResponseFormat.Json, UseHttpGet = false)]
    public string HelloWorld(Person person)
    {
        string result = "";
        try
        {
            //Do something with the person object              
            result = "Success";
        }
        catch (Exception ex)
        {
            result = ex.Message;
        }

        return result;
    }
}

Out of the box ASMX provides support for HTTP information variables like Session, HttpContext etc because ASMX can only be hosted under IIS with the same ASP.NET application usually. The web.config doesn’t really require anything in terms of settings or configuration and the service is pretty much ready to recieve requests. ASMX supports both XML and JSON with XML being the default for the format and GET being the default for the HTTP method.

This service is hosted on my local IIS with an entry in the hosts file to point the http://qvservice back to my localhost, this is so that I can host it on port 80 given that IIS doesn’t allow port sharing between multiple websites, unless you specify the host header in each website and tell the DNS to route the requests to these host headers back to 127.0.0.1 (localhost). Also, the URL is a little more memorable and distinctive than “localhost” or “127.0.0.1”.

Hosted client and service

IIS 7 Hosted client and service

Host header

Host header setting to allow customised URL to access the service instead of using “localhost” or “127.0.0.1”

2. the JS client (hosted in an .aspx page could even be a .html page) looks like this:

 
$.ajax({
    url: "http://qvservice/MyService.asmx/HelloWorld",
    type: "POST",                
    contentType: "application/json; charset=utf-8",
    data: '{"person":' + JSON.stringify(GetData()) + '}'
    }).done(function (result) {
        alert(result.d);
    }).fail(function (result) {
        alert(result.d);
    }); 

the url is URL of my ASMX web service and is literally the name of the ASMX file followed by the method name that I want to invoke. the type of the request is POST, content-Type is json and the data is JSON-ified by a call to JSON.stringify() method available in the JSON Library which is a simple JSON encoder/decoder library. I handle the “done” and “fail” events on the completion of the async request, in this case just show the result in an alert popup. The key thing to remember here is the param in double quotes – “person” should be exactly the same as the service method’s parameter name, which in this case it is. Otherwise, the service would reject the request and return a bad request error.

Since any async JS calls utilise pure HTTP for communication with the web services, ASMX services are perfectly suited for this use case since they are already HTTP aware and share HTTP information with IIS. The downside as I observed is that ASMX services are not very configurable and hence some control over the services is lost. Its a tradeoff for gaining simplicity. By default it uses basicHttpBinding which in the case of ASMX web services is not really configurable for e.g. ASMX services also don’t seem to have error tracing like WCF services. ASMX is also very tolerant of invalid JSON strings unlike WCF service which can create confusions, for e.g., in the above scenario

'{"person":' + JSON.stringify(GetData()) + '}'

is a valid JSON as per the JSON standard but ASMX allows

{'person':" + JSON.stringify(GetData()) + "}"

which is an invalid JSON string which means if I were to change my end point from ASMX to WCF keeping every thing same, it wouldn’t work because WCF adheres to JSON spec and would reject this invalid JSON with error code 400 (bad request).

Another brick wall can be hit in terms of the size of the Json string that ASMX allows by default, which is about 2097152 characters. If the size of the Json data is larger than that, then it would start rejecting requests. To allow for larger Jsons, set the following in the web.config of the service (under the configuration tags):

<system.web.extensions>
    <scripting>
        <webServices>
            <jsonSerialization maxJsonLength="#max Int32 value needed#"></jsonSerialization>
        </webServices>
    </scripting>
</system.web.extensions>

ASMX web services use the JavaScriptSerializer to convert JSON into .NET types and back and this internal serializer respects the limit set in the config file.

Since this call is happening in a cross domain way i.e. http://abc.com sending data to a service at http://service/, by default the server would not allow this for security reasons and so to allow this call, the server needs to handle something called a “pre-flight” request from the client which uses the verb “OPTIONS” to ask the server which methods and headers can be allowed and from which domains, note this is not the actual POST request that is targeted for the service.This “pre-flight” request looks like this (the one enclosed in the red box):

OPTIONS request

OPTIONS request

The server then responds with the right headers (see the second red box in the below image), I will show how to set in the Global.asax.cs file in a minute, allowing the client access with the list of actions that the client is allowed to carry out on the server. On the receipt of this response, the client then makes the actual POST request as shown in the Options Request image above sending in the JSON data to be processed by the service.

Request Response

Request Response

This is made possible by CORS – Cross Origin Resource Sharing , in which legitimate cross domain calls are allowed access with server allowing that domain and specifiying what operations the request can do on the server. W3C has a detailed explanation on this. Also Mozilla developer blog details this quite nicely as well. As mentioned previously, to allow CORS request through our server, I would add the following code in the Application_BeginRequest method of the Global.asax.cs file (add a Global.asax file if not already added):

protected void Application_BeginRequest(object sender, EventArgs e)
{
    HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*");

    if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
    {
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods","GET, POST");
        HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers","Content-Type, Accept");
        HttpContext.Current.Response.AddHeader("Access-Control-Max-Age","1728000");
        HttpContext.Current.Response.End();
    }
}

By setting the Access-Control-Allow-Origin/Methods/Headers/Max-Age header values, the server is responding to the OPTIONS request sent by the client and telling it what operations its allowed to do, once the client receives this response it then sends the actual POST request to the service which is then allowed through.

The Access-Control-Allow-Origin is set to “*” to allow requests from all domains, this should really be restricted on a per domain basis but for the purposes of this post, I will let it allow all domains.

For any other content types than application/x-www-form-urlencoded and perhaps one more??, the content-type header must be explicitly allowed by the server. So in this case the content-type of the data is set to “json” and hence the Access-Control-Allow-Headers needs to include “Content-Type”.

The max-age header makes sure that the client doesn’t have to resend an OPTIONS request for a while after doing the previous one, so effectively the client remembers the server’s response for 1728000 seconds and doesn’t issue another OPTIONS request until the expiration of this time window.

One final thing to note here is in JavaScript AJAX result, when using ASMX web services the return result is wrapped in an object variable called “d” so in order to show the return result in a JavaScript alert box for instance, you would have to access this wrapping variable like this : alert(result.d). I found this out accidentally when I tried doing alert(result) and got “undefined” in my alert box. With WCF as I will show in the next part, the return result is wrapped into a variable name that matches the name of the service method and then suffixed by the word “Result”, so in this case it would have been “HelloWorldResult”. In ASMX, if the returned result is an object of a complex type for e.g. Person , the properties/variables of that object can be accessed via this intermediate wrapping variable i.e. result.d.Name or result.d.Age or whatever. For WCF variant, replace “d” with the corresponding wrapping variable.

And that’s really it! This is the minimum that I did to make my ASMX service respond to a POST request from my jQuery AJAX client. In the final part, I will replace ASMX with WCF service and detail the configurations that were needed to make it work.

Thanks.

Tagged with: , , , , ,
Posted in Uncategorized
6 comments on “Using jQuery AJAX to talk to ASP.NET ASMX web services
  1. Sarah Touchi says:

    Hi there, I have an ASMX web service that pulls security from a database. I am finally transferring my sites to WordPress after creating them from scratch in Dreamweaver where the ASMX web service was very easy to add. Will your information above help with adding this to WordPress? Some of it is over my head. I can code but wouldnt call myself an expert. My biggest problem is where do I add this? Escpecially if I only have certain pages that are secured. I have been researching this for months and this is the first article that seems to be even close to what I need, thank you!

    • I don’t have any experience with using WordPress for sites so I am only guessing here, if you can run JavaScript on your WordPress site then you should be able to talk to ASMX web services and then my post will be able to help you with that. Unfortunately without any experience of using WordPress for websites this is the most I can help on this subject I am afraid. Apologies if that wasn’t helpful enough. Perhaps you can explain a bit more on what you are doing and how and then may be I might be able to help you a bit more.

  2. Hello,
    Very well written. I was looking out for a solution for CORS, had tried many things. This post at last helped and solved my problem.
    Thanks.

  3. Hafiz Noman says:

    its really helpful article. I was stuck in this issue and spent thousands of hours. Many Many Thanks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories
Follow Artineering on WordPress.com
Links
%d bloggers like this: