Support JSONP by extending ASP.NET Web API

Keywords: ASP.NET Javascript JSON IIS

Reference page:

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-get.html

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-post.html

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-put.html

http://www.yuanjiaocheng.net/webapi/mvc-consume-webapi-delete.html

http://www.yuanjiaocheng.net/webapi/httpclient-consume-webapi.html

The existence of Same Origin Policy results in that scripts from A can only operate DOM of "same source" pages, and pages from B will be rejected for cross-source operations. Homology policy and cross-domain resource sharing are mostly for Ajax requests. Homology policy mainly restricts Ajax requests implemented through XMLHttpRequest. If the request is a "heterogenous" address, browsers will not be allowed to read the returned content. JSONP is a common solution to solve cross-domain resource sharing. Now we use ASP.NET Web API's own extensibility to provide a "universal" JSONP implementation.

I. Jsonp MediaType Formatter

In " [CORS: Cross-domain Resource Sharing] Homology Policy and JSONP We "fill" the returned JSON object into the JavaScript callback function in the concrete Action method. Now we provide a more general implementation for JSONP through the customized MediaType Formatter.

We define the following JsonpMediaTypeFormatter type by inheriting JsonMediaTypeFormatter. Its read-only property Callback represents the JavaScript callback function name, changing the property to be specified in the constructor. In the rewritten method WriteToStream Async, for non-JSONP callbacks (callback function does not exist), we directly call the homonymous method of the base class to serialize the response object for JSON, otherwise we call WriteToStream to fill the serialized JSON string into the JavaScript callback function.

   1: public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
   2: {
   3:     public string Callback { get; private set; }
   4:  
   5:     public JsonpMediaTypeFormatter(string callback = null)
   6:     {
   7:         this.Callback = callback;
   8:     }
   9:  
  10:     public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
  11:     {
  12:         if (string.IsNullOrEmpty(this.Callback))
  13:         {
  14:             return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
  15:         }
  16:         try
  17:         {
  18:             this.WriteToStream(type, value, writeStream, content);
  19:             return Task.FromResult<AsyncVoid>(new AsyncVoid());
  20:         }
  21:         catch (Exception exception)
  22:         {
  23:             TaskCompletionSource<AsyncVoid> source = new TaskCompletionSource<AsyncVoid>();
  24:             source.SetException(exception);
  25:             return source.Task;
  26:         }
  27:     }
  28:  
  29:     private void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
  30:     {
  31:         JsonSerializer serializer = JsonSerializer.Create(this.SerializerSettings);
  32:         using(StreamWriter streamWriter = new StreamWriter(writeStream, this.SupportedEncodings.First()))
  33:         using (JsonTextWriter jsonTextWriter = new JsonTextWriter(streamWriter) { CloseOutput = false })
  35:         {
  36:             jsonTextWriter.WriteRaw(this.Callback + "(");
  37:             serializer.Serialize(jsonTextWriter, value);
  38:             jsonTextWriter.WriteRaw(")");
  39:         }
  40:     }
  41:  
  42:     public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
  43:     {
  44:         if (request.Method != HttpMethod.Get)
  45:         {
  46:             return this;
  47:         }
  48:         string callback;
  49:         if (request.GetQueryNameValuePairs().ToDictionary(pair => pair.Key, 
  50:              pair => pair.Value).TryGetValue("callback", out callback))
  51:         {
  52:             return new JsonpMediaTypeFormatter(callback);
  53:         }
  54:         return this;
  55:     }
  56:  
  57:     [StructLayout(LayoutKind.Sequential, Size = 1)]
  58:     private struct AsyncVoid
  59:     {}
  60: }

We rewrote the GetPerRequestFormatterInstance method, which is called by default when the ASP.NET Web API uses content negotiation to select a MediaTypeFormatter that matches the current request to create a MediaTypeFormatter object that is really used to serialize the response results. In the rewritten GetPerRequestFormatterInstance method, we try to get the name of the JavaScript callback function carried from the requested URL, which is a query string named "callback". If the callback function name does not exist, it returns itself directly, otherwise it returns the JsonpMediaTypeFormatter object created accordingly.

2. Applying JsonpMediaTypeFormatter to ASP.NET Web API

Next, we demonstrate the limitations of the homology policy on cross-domain Ajax requests through a simple example. As shown in the right figure, we created two Web applications in the same solution using Visual Studio. As can be seen from the project name, WebApi and MvcApp are ASP.NET Web API and MVC application respectively, and the latter is the caller of Web API. We directly use the default IIS Express as the host of the two applications and fix the port numbers: WebApi and MvcApp are "3721" and "9527" respectively, so the URI s pointing to the two applications can not be homologous.

In WebApi applications, we define the following Contacts Controller type inherited from ApiController, which has the unique Action method GetAllContacts that returns a list of contacts.

   1: public class ContactsController : ApiController
   2: {
   3:     public IEnumerable<Contact> GetAllContacts()
   4:     {
   5:         Contact[] contacts = new Contact[]
   6:         {
   7:             new Contact{ Name="Zhang San", PhoneNo="123", EmailAddress="zhangsan@gmail.com"},
   8:             new Contact{ Name="Li Si", PhoneNo="456", EmailAddress="lisi@gmail.com"},
   9:             new Contact{ Name="Wang Wu", PhoneNo="789", EmailAddress="wangwu@gmail.com"},
  10:         };
  11:         return contacts;
  12:     }
  13: }
  14:  
  15: public class Contact
  16: {
  17:     public string Name { get; set; }
  18:     public string PhoneNo { get; set; }
  19:     public string EmailAddress { get; set; }
  20: }

Now we use the following program to create the JsonpMediaTypeFormatter object in Global.asax of the WebApi application and add the currently registered MediaTypeFormatter list. To make it a priority, we put the JsonpMediaTypeFormatter object at the top of the list.

   1: public class WebApiApplication : System.Web.HttpApplication
   2: {
   3:     protected void Application_Start()
   4:     {
   5:         GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter ());
   6:         //Other operations
   7:     }
   8: }

Next, we define the following HomeController in the MvcApp application. The default Action Method Index renders the corresponding View.

   1: public class HomeController : Controller
   2: {
   3:     public ActionResult Index()
   4:     {
   5:         return View();
   6:     }
   7: }

The definition of the Action Method Index corresponding to View is shown below. Our goal is to call the Web API defined above in the form of an Ajax request after the page is successfully loaded to get a list of contacts and present itself on the page. As shown in the code snippet below, we call the $.ajax method directly and set the dataType parameter to "jsonp".

   1: <html>
   2: <head>
   3:     <title>contact list</title>
   4:     <script type="text/javascript" src="@Url.Content("~/scripts/jquery-1.10.2.js")"></script>
   5: </head>
   6: <body>
   7:     <ul id="contacts"></ul>
   8:     <script type="text/javascript">
   9:         $(function ()
  10:         {
  11:             $.ajax({
  12:                 Type       : "GET",
  13:                 url        : "http://localhost:3721/api/contacts",
  14:                 dataType   : "jsonp",
  15:                 success    : listContacts
  16:             });
  17:         });
  18:  
  19:         function listContacts(contacts) {
  20:             $.each(contacts, function (index, contact) {
  21:                 var html = "<li><ul>";
  22:                 html += "<li>Name: " + contact.Name + "</li>";
  23:                 html += "<li>Phone No:" + contact.PhoneNo + "</li>";
  24:                 html += "<li>Email Address: " + contact.EmailAddress + "</li>";
  25:                 html += "</ul>";
  26:                 $("#contacts").append($(html));
  27:             });
  28:         }
  29:     </script>
  30: </body>
  31: </html>

After running the ASP.NET MVC program directly, you will get the output shown in the following figure, and the contact list obtained by calling the Web API across domains will be displayed normally.

3. Request and response to JSONP

The Ajax request and response content for JSONP shown below. You can see that the name of the JavaScript callback function is provided in the request URL through the query string "callback", and the main part of the response is not simply a JSON object, but a function call statement generated by filling the JSON object into the callback return.

   1: GET http://localhost:3721/api/contacts?callback=jQuery110205729522893670946_1386232694513 &_=1386232694514 HTTP/1.1
   2: Host: localhost:3721
   3: Connection: keep-alive
   4: Accept: */*
   5: User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36
   6: Referer: http://localhost:9527/
   7: Accept-Encoding: gzip,deflate,sdch
   8:  
   9: HTTP/1.1 200 OK
  10: Cache-Control: no-cache
  11: Pragma: no-cache
  12: Content-Type: application/json; charset=utf-8
  13: Expires: -1
  14: Server: Microsoft-IIS/8.0
  15: X-AspNet-Version: 4.0.30319
  16: X-SourceFiles: =?UTF-8?B?RTpc5oiR55qE6JGX5L2cXEFTUC5ORVQgV2ViIEFQSeahhuaetuaPreenmFxOZXcgU2FtcGxlc1xDaGFwdGVyIDE0XFMxNDAzXFdlYkFwaVxhcGlcY29ud?=
  17: X-Powered-By: ASP.NET
  18: Date: Thu, 05 Dec 2013 08:38:15 GMT
  19: Content-Length: 248
  20:  
  21: jQuery110205729522893670946_1386232694513([{"Name":"Zhang San","PhoneNo":"123","EmailAddress":"zhangsan@gmail.com"},{"Name":"Li Si","PhoneNo":"456","EmailAddress":"lisi@gmail.com"},{"Name":"Wang Wu","PhoneNo":"789","EmailAddress":wangwu@gmail.com}])
   

 

CORS Series Articles
[1] Homology Policy and JSONP
[2] Using Extension to Support JSONP with ASP.NET Web API
[3] W3C CORS Specification
[4] Using Extension to Support CORS with ASP.NET Web API
[5] ASP.NET Web API's own support for CORS: Starting with examples
[6] ASP.NET Web API's Support for CORS: Definition and Provision of CORS Authorization Policy
[7] ASP.NET Web API's Support for CORS: Implementation of CORS Authorization Verification
[8] ASP.NET Web API's own support for CORS: CorsMessageHandler

Posted by acac on Sat, 22 Dec 2018 18:54:06 -0800