Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JSONP & ASMX Web Service

I'm pulling my hair out trying to figure out how to make a JSONP call to an ASMX web service using jQuery. These are just some of the pages that I've already read and haven't found any solution:

How to call external webservice using jquery "jsonp"?

Posting cross-domain JSON to ASP.NET with jQuery

Error while accessing ASP.net webservice using JQuery - JSONP

Set Headers with jQuery.ajax and JSONP?

http://www.codeproject.com/Articles/43038/Accessing-Remote-ASP-NET-Web-Services-Using-JSONP

http://encosia.com/using-jquery-to-consume-aspnet-json-web-services/

etc...

Here is my sample .NET web method:

[WebMethod]
[ScriptMethod(UseHttpGet = true, ResponseFormat = ResponseFormat.Json)]
public void GetEmployee(string employeeId, string callback)
{
    // Get the employee object from the Factory.
    Employee requestedEmployee = EmployeeFactory.GetEmployee(employeeId);

    if(requestedEmployee != null)
    {
        // Return the padded JSON to the caller.
        CrossDomainUtility.SendJsonP(callback, requestedEmployee.ToJson());
    }
}

Here is SendJsonP():

public static void SendJsonP(string callback, string json)
{
    // Clear any response that has already been prepared.
    HttpContext.Current.Response.Clear();

    // Set the content type to javascript, since we are technically returning Javascript code.
    HttpContext.Current.Response.ContentType = "application/javascript";

    // Create a function call by wrapping the JSON with the callback function name.
    HttpContext.Current.Response.Write(String.Format("{0}({1})", callback, json));

    // Complete this request, to prevent the ASMX web service from doing anything else.
    HttpContext.Current.ApplicationInstance.CompleteRequest();
}   

And here is some sample jquery code:

$.ajax({
    url: 'http://devserver/service/service.asmx/GetEmployee',
    dataType: 'jsonp',
    contentType: 'application/json',
    data: { employeeId: '123456789' }
});

I have the web service decorated with [ScriptService] and I have my web.config configured to handle *.asmx using the ScriptHandlerFactory.

I've tried using the built-in JSON serialization that ASMX uses when Content-Type is 'application/json', but there are a couple problems: it can't work for JSONP due to the padding that's required to be wrapped around the JSON which .NET doesn't support. It also doesn't work because in order to serialize JSON, ASMX expects a 'ContentType: application/json' header, but jQuery ignores ContentType headers when sending GET requests (presumably because it isn't sending any content). I've tried setting Request.ContentType = "application/json" in Global.asax Application_BeginRequest() but that didn't do anything. I've also tried setting the request header in jQuery using beforeSend() with no luck.

So since I couldn't get it to work easily using the built-in .NET pipeline, I rolled-my own technique that performs raw writes to the Response body (hence the SendJsonP() method). I'm still having problems though, because even though the GetEmployee() web method is not returning a value, .NET is throwing serialization errors because it is trying to serialize the object to XML since I can't pass a ContentType of 'application/json' with GET requests.

So, since I can't get jQuery to add the ContentType no matter what I do, I wanted to test my web service by just creating manual requests using Fiddler2:

GET http://devserver/service/service.asmx/GetEmployee?callback=createMember&memberId=123456789
User-Agent: Fiddler
Content-Type: application/json
Host: devserver

... and it gives the following error because my parameters are not JSON:

{"Message":"Invalid JSON primitive: createMember [....] }

So after all that, I'm left with a few questions:

  1. Is there a way to use built-in .NET serialization to apply padding to JSON and return it to the client?

  2. Since it appears that I have to roll my own, how should my query string look when sending a JSONP query with parameters to an ASMX page? It has to be in JSON format but I've tried the following and received "Invalid JSON primitive" errors:

    GetEmployee?{callback:"createMember", memberId:"99999999"}

    GetEmployee?callback={callback:"createMember"}&memberId={memberId:"123456789"}

  3. Is there any way to have jQuery send a ContentType header with JSONP GET requests?

like image 926
Vince Fedorchak Avatar asked Dec 14 '12 18:12

Vince Fedorchak


1 Answers

I've just decided to process JSONP requests manually. In my solution the user must provide two query string parameters via a GET request in order to indicate that they want JSONP results: callback=callbackFunctionName, and jsonp=true. If both of those are received I will process it manually, otherwise the request will continue onto the standard ASMX processor.

I've created a new JsonPUtility helper class, which I've added as a call in the HttpApplication.BeginRequest event:

public class Global : System.Web.HttpApplication
{
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        // Requests for JSONP requests must be handled manually due to the limitations of JSONP ASMX.
        JsonPUtility.ProcessJsonPRequest();
    }
}

And here is the JsonPUtility class:

/*
 *  JSON is Javascript Object Notation, a standard way of serializing objects in Javascript and
 *  other languages.  For more information see http://www.json.org/.
 * 
 *  JSONP is a technique to enable the execution of Javascript that resides in a different domain.  It 
 *  works by exploiting the exception granted to the <script> tag which allows content to be loaded
 *  from a different domain.  By contrast, making "regular" AJAX calls to a different domain will
 *  not work, usually throwing an "Access Denied" or "No Transport" error.
 *  
 *  JSONP (the "P" stands for "Padding") is regular JSON wrapped in a Javascript function call (the
 *  "Padding").  Take for example this standard JSON object:
 *      { "Name" : "John", "Age" : 14, "Gender" : "Male" }
 *      
 *  JSONP will turn that JSON into a valid Javascript function call by using the JSON as an argument
 *  to the callback function provided by the caller.  For example, if the caller provides a callback
 *  value of 'processResults', the resulting JSONP looks like this:
 *      processResults({ "Name" : "John", "Age" : 14, "Gender" : "Male" });
 *      
 *  The processResults() function will then be able to use the JSON object just like a regular object.
 *  Note that the callback function must be implemented on the page that receives the JSONP, otherwise
 *  a standard Javascript error will occur.
 *  
 *  The real "trick" to cross-domain script execution is dynamically creating a "script" tag on the
 *  client for every JSONP request, using the web service URL as the "src" attribute.  This will cause
 *  the browser to automatically download and execute the script that is loaded from the URL,
 *  effectively bypassing the same-domain origin policy.
 */
public static class JsonPUtility
{
    /*
     * SendJsonP(string callback, string json)
     *  
     *  This method takes the provided 'json' string, wraps it so that it is a parameter to the 'callback'
     *  function, clears any existing response text, writes the resulting Javascript code to the 
     *  response, and ends the response.
     *  
     *  For example, given these two parameters...
     *      callback    = "callbackFunction"
     *      json        = "{ 'FOO': 'BAR', 'JOO': 'MAR' }"
     *  
     *  ... the following code is returned to the client in an HTTP response with a content-type of
     *  'application/javascript':
     *      callbackFunction({ 'FOO': 'BAR', 'JOO': 'MAR' });
     *      
     */
    public static void SendJsonP(string callback, string json)
    {
        // Clear any response that has already been prepared.
        HttpContext.Current.Response.Clear();

        // Set the content type to javascript, since we are technically returning Javascript code.
        HttpContext.Current.Response.ContentType = "application/javascript";

        // Create a function call by wrapping the JSON with the callback function name.
        HttpContext.Current.Response.Write(String.Format("{0}({1});", callback, json));

        // Complete this request, to prevent the ASMX web service from doing anything else.
        HttpContext.Current.ApplicationInstance.CompleteRequest();
    }

    /*
     * bool IsJsonPRequest()
     * 
     *  Determines whether or not the current request is for JSONP javascript code.
     *  
     *  This is the criteria for making a JSONP request to this web service:
     *      1. Include the jsonp parameter.  Its value is not important - we recommend using jsonp=true
     *         to increase clarity.
     *      2. Include the callback=string parameter so we know what function call to wrap around
     *         the requested JSON.
     */
    public static bool IsJsonPRequest()
    {
        // Store the context to the current request.
        var request = HttpContext.Current.Request;

        // If a 'jsonp' or a 'callback' parameter was not provided, this isn't a JSONP request.
        if (request.QueryString["jsonp"] == null || String.IsNullOrEmpty(request.QueryString["callback"]))
            return false;

        // Since both parameters were provided, this is a jsonp request.
        return true;
    }

    /*
     * ProcessJsonPRequest()
     * 
     *  Manual processing is required for JSONP requests due to limitations in ASMX web services.
     */
    public static void ProcessJsonPRequest()
    {
        // If this isn't a JSONP request, simply return and continue regular request processing.
        if (!IsJsonPRequest())
            return;

        // Store the context to the HTTP request.
        var request = HttpContext.Current.Request;

        // Store the callback function that will be wrapped around the JSON string.
        string callback = request.QueryString["callback"];

        // Create a place to store the object that will be serialized into JSON.
        object objectForJson = null;

        // Store the web service method name that is being requested.  It is always going to follow the
        // final slash after the .asmx extension, and will continue until the question mark that marks
        // the query string.
        int     methodNameStartIndex = request.RawUrl.ToUpper().IndexOf(".ASMX/") + 6;
        int     methodNameLength = (request.RawUrl.IndexOf("?")) - methodNameStartIndex;
        string  requestMethod = request.RawUrl.Substring(methodNameStartIndex, methodNameLength);

        // Create a place to store the string ID of the object that is going to be looked-up.
        string lookupId = null;

        // Based on the request URL, figure out the method that will create a reference for the objectForJson variable.
        switch (requestMethod)
        {
            case "GetEmployee":
                // Get the employee's ID from the query string.
                lookupId = request.QueryString["employeeId"];

                // If the employee ID was provided, get a Employee object.
                if (!String.IsNullOrEmpty(lookupId))
                    objectForJson = Factory.GetEmployee(lookupId);

                break;

            case "GetManager":
                // Get the manager's ID from the query string.
                lookupId = request.QueryString["managerId"];

                // If the manager ID was provided, get a Manager object.
                if (!String.IsNullOrEmpty(lookupId))
                    objectForJson = Factory.GetManager(lookupId);

                break;

            case "GetOrder":
                // Get the order ID from the query string.
                lookupId = request.QueryString["orderId"];

                // If the order ID was provided, get the  object.
                if (!String.IsNullOrEmpty(lookupId))
                    objectForJson = Factory.GetOrder(lookupId);

                break;

            default:
                // If the request method wasn't handled, throw an exception.
                throw new ArgumentException("Unknown request method '" + requestMethod + "'.");
        }

        // Create a .NET framework object to serialize the object into JSON.
        JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();

        // Serialize the object into JSON.  If objectForJson is null, the callback function will be passed a parameter of null (e.g. callback(null)).
        string json = jsonSerializer.Serialize(objectForJson);

        // Send the JSONP string back to the caller.
        SendJsonP(callback, json);
    }
} 

I hope this can help somebody in the future.

Thanks, Vince

like image 90
Vince Fedorchak Avatar answered Oct 08 '22 05:10

Vince Fedorchak