Click here to Skip to main content
15,884,237 members
Articles / Web Development / ASP.NET

Accessing Remote ASP.NET Web Services Using JSONP

Rate me:
Please Sign up or sign in to vote.
4.90/5 (21 votes)
13 Oct 2009CPOL5 min read 151.4K   3.3K   40   37
This article digs into the details of accessing an ASP.NET ASMX Web Service from a remote AJAX client. It explains the configuration needed in the server side and describes a sample JQuery client that is able to call JSONP enabled services.

The problem:

You cannot call remote ASP.NET Web Service methods from a JavaScript, AJAX client.

Example:

You have a Web Service at this address: http://a.com/service.asmx and you've configured the service to work with AJAX clients:

C#
[WebService
(Namespace = "http://www.hyzonia.com/gametypes/PopNDropLikeGame/WS2")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class GameService : System.Web.Services.WebService
{
    [WebMethod(EnableSession = true)]
    public GameSessionResponse CreateGameSession(Guid questId)
    {
...
    }
}

And it works fine when you call its methods from a web page that is in this address: http://a.com/page.htm:

JavaScript
$.ajax({
        type: "POST",
        url: "GameService.asmx/CreateGameSession",
        data: "{questId: '" + questId + "'}",
        cache: false,
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function(response) {
            Game._onSessionGot(response.d);
        }
    });

But the very same client-side code doesn’t work from this address: http://b.clom/page.htm.

The problem in depth:

At first, it is a silly problem, for me it is an overprotection. After all, Web Services are meant to be called by remote clients. The fact that browsers block access to Web Services by AJAX calls is clearly contrary to the purpose of Web Services.

Interestingly, browser extensions like Flash and Silverlight also, by default, block remote Web Services, but they provide a workaround. Unfortunately, no browser by date supports this work around for XMLHttpRequests. This "security measure" seems odder when we notice that it is perfectly correct to import a JavaScript code snippet from another domain using a script tag:

HTML
<script
  src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"
  type="text/javascript">
</script>

The solution:

As it was said, Flash and Silverlight both support remote calls. You just need a client access policy file to be hosted at the root of a.com (http://a.com/clientaccesspolicy.xml):

XML
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="SOAPAction">
        <domain uri="*"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

This file allows remote calls to be made from any other domain.

But in many situations, we want to call the Web Service methods directly from AJAX clients. This need was the cause of the development of JSONP (JSON with padding) protocol. As it was discussed, it is correct to have a <script> element that loads a script from another domain. On the other hand, you may know that it is possible to load scripts dynamically by a simple JavaScript trick (writing <script> tags) or using this jQuery plug in. Now the bulbs are flickering! The solution is to access the JSON Web Service by the src attribute of a <script> element. This is the whole idea behind JSONP.

But there are a couple of problems needed to be solved for ASP.NET ASMX Web Services before we can use them in a JSONP scenario.

  1. ASP.NET Web Services by default only accept POST requests; a <script src=""> element, produces a GET request.
  2. The result of the web method call must conform to JSONP, and as you can guess, ASP.NET 3.5 by default doesn’t support it.

The solution to the first problem may seem trivial, we can easily enable GET calls to web methods using the [ScriptMethod(UseHttpGet = true)] attribute. The immediate problem is that when we mark a web method by this attribute, it can only be called by GET requests. And remember, other clients (actually anything other than JSONP clients) are supposed to communicate with the web service by POST requests. I usually end up inheriting from the original Web Service and marking web methods by the [ScriptMethod(UseHttpGet = true)] attribute in the derived class. Therefore, I will have two ASMX Web Services, one using the original class (expecting POST requests) and the other using the derived class (expecting GET requests).

C#
[WebMethod(), ScriptMethod(UseHttpGet = true)]
public override GameSessionResponse CreateGameSession(Guid questId)
{
   return base.CreateGameSession(questId);
}

Note you may need to add this code snippet in web.config:

XML
<system.web>
 <webServices>
   <protocols>
     <add name="HttpGet"/>
   </protocols>
 </webServices></system.web>

There's another problem to be addressed in the client side. The client should call the web method using the correct URL (it has to pass the correct query string that could be deserialized back to .NET objects in the server side). In case of POST requests, I'm used to JSON2 library to post data to ASP.NET ASMX Web Services. JQuery $.AJAX method (when it is configured to use JSONP, using dataType: "jsonp") creates query string parameters for the data objects it receives. But the result is not usable for ASMX Web Services.

Luckily, there's a ready to use JQuery plug-in (jMsAjax) that has the required algorithms for serializing a JavaScript object into a query string that can be parsed by ASP.NET Web Services.

Using the plug-in, I created this function to serialize JavaScript objects into query strings:

JavaScript
$.jmsajaxurl = function(options) {
    var url = options.url;
    url += "/" + options.method;
    if (options.data) {
       var data = ""; for (var i in options.data) {
       if (data != "")
         data += "&"; data += i + "=" + 
                 msJSON.stringify(options.data[i]);
       }
       url += "?" + data; data = null; options.data = "{}";
   }
   return url;
};

You will need jMsAjax for this code snippet to work.

Finally, this is a sample of a client side code using JQuery that calls an ASMX Web Service using JSONP:

JavaScript
var url = $.jmsajaxurl({
    url: "http://hiddenobjects.hyzonia.com/services/GameService3.asmx",
    method: "Login",
    data: { email: "myemail@mydomain.com", password: "mypassword" }
});

$.ajax({
    cache: false,
    dataType: "jsonp",
    success: function(d) { console.log(d); },
    url: url + "&format=json"
});

Or equivalently:

JavaScript
$.getJSON(url + "&callback=?&format=json", function(data) {
    console.log(data);
});

When you call an ASP.NET Web Service method (that is configured to receive GET requests) using a code similar to the above, it returns in XML. The problem is that the Web Service expects to receive a request that has a content type of "application/json; charset=utf-8" and the <script> element simply doesn't add this content type to the request. There's a little thing we can do at the client side. The easiest way to resolve this problem is to use an HTTP module. The HTTP module should add this content type to the requests before they are processed by the Web Service handler.

On the other hand, a JSONP client expects that the Web Service returns the call by a string like this:

JavaScript
nameOfACallBackFunction(JSON_OBJECT_WEB_METHOD_RETURNED)

nameOfACallBackFunction must be given to the server by a parameter in the query string. Different JSONP compatible Web Services use different names for this parameter, but usually it is named 'callback'. At least, this is what $.ajax() automatically adds to the request in JSONP mode.

We have to modify the response stream that the server is returning. Luckily, in ASP.NET, it is easy to apply a filter to the response.

I slightly modified this HTTP module that I originally grabbed from a post in elegantcode.com, to improve its performance:

C#
public class JsonHttpModule : IHttpModule
{
    private const string JSON_CONTENT_TYPE = 
            "application/json; charset=utf-8";

    public void Dispose()
    {
    }

    public void Init(HttpApplication app)
    {
        app.BeginRequest += OnBeginRequest;
        app.ReleaseRequestState += OnReleaseRequestState;
    }

    bool _Apply(HttpRequest request)
    {
        if (!request.Url.AbsolutePath.Contains(".asmx")) return false;
        if ("json" != request.QueryString.Get("format")) return false;
        return true;
    }

    public void OnBeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        if (!_Apply(app.Context.Request)) return;
        
        // correct content type of request
        if (string.IsNullOrEmpty(app.Context.Request.ContentType))
        {
            app.Context.Request.ContentType = JSON_CONTENT_TYPE;
        }
    }

    public void OnReleaseRequestState(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        if (!_Apply(app.Context.Request)) return;

        // apply response filter to conform to JSONP
        app.Context.Response.Filter = 
            new JsonResponseFilter(app.Context.Response.Filter, app.Context);
    }
}

public class JsonResponseFilter : Stream
{
    private readonly Stream _responseStream;
    private HttpContext _context;

    public JsonResponseFilter(Stream responseStream, HttpContext context)
    {
        _responseStream = responseStream;
        _context = context;
    }

    //...

    public override void Write(byte[] buffer, int offset, int count)
    {
        var b1 = Encoding.UTF8.GetBytes(
          _context.Request.Params["callback"] + "(");
        _responseStream.Write(b1, 0, b1.Length);
        _responseStream.Write(buffer, offset, count);
        var b2 = Encoding.UTF8.GetBytes(");");
        _responseStream.Write(b2, 0, b2.Length);
    }

    //...
}

This HTTP module will be applied to each request to an .asmx file that has a format=json in its query string. Note that you have to update web.config:

XML
<system.web><httpModules><add name="JSONAsmx" type="JsonHttpModule, App_Code"/>
  </httpModules>
</system.web>

for IIS6, and:

XML
<system.webServer>
  <modules><add name="JSONAsmx" type="JsonHttpModule, App_Code"/>
  </modules></system.webServer>

for IIS7.

Now to test it, let's open the Web Service in a browser window; in my example, http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email=e@e.com&password=p should return in XML, and http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email="e@e.com"&password="p"&format=json&callback=myCallBackFunc will return:

JavaScript
myCallBackFunc({"d":{"__type":"HLoginResponse",
   "isSuccessful":false,"error":false,"authSessionId":null,
   "nickName":null,"score":0}});

Don't worry about myCallBackFunc, JQuery nicely manages it so that the whole business is behind the scene and you can use the $.ajax success callback the very same way you use it for a normal AJAX call.

We should note that JSONP has its own problems, especially… yes... in IE! All versions of Internet Explorer have a 2083 character limit for the URL of a request. It means that you cannot send large data in GET requests to the server. Sometimes this limitation leaves us with no choice but to use Flash or create a proxy to the remote Web Service in the local domain.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United Arab Emirates United Arab Emirates
I love messing around with JavaScript, my discovery of the power of JavaScript was the biggest discovery since the time I found out I like astronomy. I spent most of my life shooting heavenly bodies, studying physics, organizing or just being a nice member of amateur astronomy groups and writing about astronomy.

I believe in fast and agile development styles, things just change for a small company before almost any design could be finalized. The programs that I create evolve. An evolved program works better and better by time, but finally nobody knows how!

Seriously, I’m a software architect with wild ideas. I enjoy working on large distributed systems, anything from remotely controlling a telescope to a network of games distributed all over the internet.

Here is the latter one, Hyzonia; I invested a good amount of time on designing and developing it. You might enjoy it if you want to spice up your website with some games or if you’re looking for a good reason to develop a casual game.

Here's my blog.

Comments and Discussions

 
Questionmy vote 5 Pin
Vincenzo Malvone5-Nov-15 5:18
Vincenzo Malvone5-Nov-15 5:18 
my vote 5
QuestionIssues accessing remote ASP.NET Web Services Using JSONP Pin
imaxo0117-Apr-15 7:57
imaxo0117-Apr-15 7:57 
QuestionUsing with JQuery AutoComplete Pin
clprogrammer3-Jul-14 9:42
clprogrammer3-Jul-14 9:42 
QuestionThanks Pin
pepegrillo1135-Oct-13 16:53
pepegrillo1135-Oct-13 16:53 
GeneralMy vote of 5 Pin
kaushal s10-Jul-13 0:22
kaushal s10-Jul-13 0:22 
QuestionPLEASE HELP ASAP !!! Pin
testho@yahoo.ocm2-Sep-12 0:20
testho@yahoo.ocm2-Sep-12 0:20 
AnswerRe: PLEASE HELP ASAP !!! Pin
Homam Hosseini2-Sep-12 0:32
Homam Hosseini2-Sep-12 0:32 
GeneralRe: PLEASE HELP ASAP !!! Pin
testho@yahoo.ocm2-Sep-12 2:28
testho@yahoo.ocm2-Sep-12 2:28 
GeneralRe: PLEASE HELP ASAP !!! Pin
testho@yahoo.ocm2-Sep-12 19:10
testho@yahoo.ocm2-Sep-12 19:10 
AnswerRe: PLEASE HELP ASAP !!! Pin
testho@yahoo.ocm3-Sep-12 3:10
testho@yahoo.ocm3-Sep-12 3:10 
QuestionRestfull Pin
joseph_man16-Jul-12 3:21
joseph_man16-Jul-12 3:21 
QuestionMy vote of 5 Pin
hristiyan.dimov28-Feb-12 1:12
hristiyan.dimov28-Feb-12 1:12 
GeneralMy vote of 5 Pin
Compufreak45624-Feb-12 1:28
Compufreak45624-Feb-12 1:28 
QuestionMUST UPDATE CODE WITH CORRECTION OF 15:17 14 Apr '10 Pin
faber7528-Oct-11 9:16
faber7528-Oct-11 9:16 
GeneralMy vote of 5 Pin
marcelsnews14-Oct-11 4:27
marcelsnews14-Oct-11 4:27 
GeneralRe: My vote of 5 Pin
erhan demirci2-Feb-12 2:06
erhan demirci2-Feb-12 2:06 
GeneralRe: My vote of 5 Pin
marcelsnews2-Feb-12 3:06
marcelsnews2-Feb-12 3:06 
GeneralRe: My vote of 5 Pin
erhan demirci2-Feb-12 3:10
erhan demirci2-Feb-12 3:10 
GeneralRe: My vote of 5 Pin
marcelsnews2-Feb-12 11:43
marcelsnews2-Feb-12 11:43 
GeneralRe: My vote of 5 Pin
erhan demirci2-Feb-12 21:52
erhan demirci2-Feb-12 21:52 
GeneralRe: My vote of 5 Pin
erhan demirci7-Feb-12 3:04
erhan demirci7-Feb-12 3:04 
GeneralMy vote of 5 Pin
Member 56545114-Apr-11 1:56
Member 56545114-Apr-11 1:56 
QuestionInvalid JSON Primitive? Pin
KenLG22-Dec-10 19:44
KenLG22-Dec-10 19:44 
GeneralProblem with Internet Explorer Pin
pawel198527-Oct-10 2:55
pawel198527-Oct-10 2:55 
GeneralBug when returning a large amount of JSON Pin
justintoth11-Mar-10 16:13
justintoth11-Mar-10 16:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.