Calling Web Services via AJAX - Part 2
Looks like this is an interesting topic to a lot of people since part 1 of this series made it to the front page of Digg today. Good thing that I was already working on part 2 :)
In Calling Web Services via AJAX - Part 1, I highlighted how easy it is to use client-side script to call an ASP.NET web service leveraging the ASP.NET AJAX library. In this post, I will show how to replace the ASMX service using a WCF service.
What is WCF?
Most people think of Windows Communication Foundation (WCF) and WS-* synonymously. To clarify, WS-* refers to the set of specifications (WS-Addressing, WS-MetadataExchange, WS-Security, WS-ReliableMessaging) that compose the set of web services specifications.
Windows Communication Foundation, by contrast, is an implementation of those specifications. The confusion is partially earned as the first implementation of WCF was squarely positioned as the most complete implementation of the WS-* stack, and using POX with the first version of WCF was a bit of a kludge. Not to mention the name confusion between WCF and WS-*... blame that on the marketing folks since many still call it by its code-name "Indigo".
The architecture of WCF separates how a message is serialized from its underlying transport, and the internal representation of a message for WCF is the XML Infoset. This means that, despite WCF's internal representation of a message, it can be serialized using a wide variety of formats including text or binary. The binary serialization is useful for implementations such WCF's binary format or interoperability formats such as Fast Infoset. The text serialization capabilities include a number of formats as well including SOAP (1.2 or 1.2), POX, or even JSON... which we will look at closer in this post.
.NET 3.5 introduces several important features for WCF, including an easy-to-use HTTP web programming model, support for creation of RESTful services, UriTemplates, and support for JSON encoding. WCF makes programming REST, POX, and AJAX just as easy as programming SOAP.
Exposing a WCF Service Using JSON
I have covered exposing a WCF service using JSON several times over in previous blog entries. Let's look at a simple example.
using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
[ServiceContract(Namespace = " ")]
public interface Service
{
[WebInvoke(Method="POST",ResponseFormat=WebMessageFormat.Json)]
[OperationContract]
string HelloWorld();
}
public class ServiceImpl : Service
{
public string HelloWorld()
{
return "Hello World";
}
}
This service says absolutely nothing about it will be hosted, says nothing about JSON or XML or SOAP or HTTP. This is what I meant before by the WCF architecture separating concerns of addressing, serialization, and activation. Those capabilities are expressed in the web.config for the service.
<system.serviceModel>
<services>
<service name="ServiceImpl">
<endpoint
address=""
binding="webHttpBinding"
contract="Service"
behaviorConfiguration="WCFServiceAspNetAjaxBehavior"/>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior
name="WCFServiceAspNetAjaxBehavior">
<enableWebScript />
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
WCF 3.5 introduces the webHttpBinding to provide great interop with the ASP.NET AJAX library, specifically providing support for the "D" JSON encoding (more on that in a minute). The webHttpBinding tells WCF that the endpoint should be accessible using an HTTP web programming model (using HTTP verbs such as GET, POST, and the like). We also configure the enableWebScript behavior which tells WCF to serialize messages using JSON encoding.
Just like with ASMX, we can ask the service to hand us a pre-built JavaScript proxy by appending "/js" to the end of the querystring.
https://localhost:46812/WebSite7/Service.svc/js
The WCF-generated JavaScript proxy is identical to what is generated from ASMX save for the path being different, noted in the second to last line that uses set_path to point to the service.svc file instead of the service.asmx file.
Service=function() {
Service.initializeBase(this);
this._timeout = 0;
this._userContext = null;
this._succeeded = null;
this._failed = null;
}
Service.prototype={
_get_path:function() {
var p = this.get_path();
if (p) return p;
else return Service._staticInstance.get_path();},
HelloWorld:function(succeededCallback, failedCallback, userContext) {
return this._invoke(this._get_path(), 'HelloWorld',false,{},succeededCallback,failedCallback,userContext); }}
Service.registerClass('Service',Sys.Net.WebServiceProxy);
Service._staticInstance = new Service();
Service.set_path = function(value) { Service._staticInstance.set_path(value); }
Service.get_path = function() { return Service._staticInstance.get_path(); }
Service.set_timeout = function(value) { Service._staticInstance.set_timeout(value); }
Service.get_timeout = function() { return Service._staticInstance.get_timeout(); }
Service.set_defaultUserContext = function(value) { Service._staticInstance.set_defaultUserContext(value); }
Service.get_defaultUserContext = function() { return Service._staticInstance.get_defaultUserContext(); }
Service.set_defaultSucceededCallback = function(value) { Service._staticInstance.set_defaultSucceededCallback(value); }
Service.get_defaultSucceededCallback = function() { return Service._staticInstance.get_defaultSucceededCallback(); }
Service.set_defaultFailedCallback = function(value) { Service._staticInstance.set_defaultFailedCallback(value); }
Service.get_defaultFailedCallback = function() { return Service._staticInstance.get_defaultFailedCallback(); }
Service.set_path("/WebSite7/Service.svc");
Service.HelloWorld= function(onSuccess,onFailed,userContext) {Service._staticInstance.HelloWorld(onSuccess,onFailed,userContext); }
We save that generated proxy into a file called proxy.js, and then our actual UI implementation code remains unchanged with 3 simple lines of JavaScript code.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
<title>Calling a Service</title>
<script src="MicrosoftAjax.js"type="text/javascript" language="javascript"></script>
<script src="proxy.js" type="text/javascript" language="javascript"></script>
<script language="javascript" type="text/javascript">
function onClick()
{
Service.HelloWorld(onSuccess, onFailure);
}
function onSuccess(sender, e)
{
alert(sender);
}
function onFailure(sender, e)
{
alert("Problem retrieving XML data");
}
</script>
</head>
<body>
<div>
<input type="button" onclick="onClick();return false;" />
</div>
</body>
</html>
Again, this HTML would look identical if it were generated from JSP, ColdFusion, PHP, Ruby, whatever, because it is all just HTML and JavaScript.
When you view this page and click the button, an alert box is shown with "Hello World" in it. What that does behind the scenes is issue an HTTP POST to the following URL:
https://localhost:46812/WebSite7/Service.svc/HelloWorld
That HTTP POST also includes the following HTTP headers, useful for using a tool like Fiddler to create your own HTTP request.
Content-Type: application/json; charset=utf-8
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en-us
User-Agent : Mozilla/4.0 (compatible; MSIE 7.0)
UA-CPU: x86
There's no body in our example here, but there would be if our service include parameters. The service accepts this request, maps it to the appropriate service and method, then uses the information in the configuration file to serialize the reply. We specified the enableWebScript behavior, which told WCF to serialize the result as JSON. The HTTP response looks like this:
HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Wed, 09 Apr 2008 13:38:05 GMT
X-AspNet-Version: 2.0.50727
Cache-Control: private
Content-Type: application/json; charset=utf-8
Content-Length: 19
Connection: Close
{"d":"Hello World"}
Which leads us to an interesting discussion.
What's With the "d" Prefix in the JSON Result in ASP.NET AJAX?
There's a potential security exploit called "array object constructor hijacking" (several interesting articles listed in that search result). There's a really good write-up of the vulnerability and exploits in Joe Walker's post, "JSON is not as safe as people think it is". The reason that ASP.NET AJAX uses the "d" wrapper is to protect against this particular security vulnerability. If your service returned an array, it would be succeptible to this attack without a type wrapping the array, hence the "d" wrapper. In most cases, the "d" wrapper should be transparent to you.
In case you run into situations where you need tighter control, WCF provides 2 ways to use JSON in WCF. The first is as described above using the webHttpBinding and the enableWebScript behavior. The second is to use the webHttpBehavior and UriTemplates. Justin Smith explains the different design goals for the two different scenarios: enableWebScript behavior was designed to simplify working with the ASP.NET AJAX stack, while webHttpBehavior provides control over the UriTemplate.
It should be noted that using the webHttpBehavior and WebInvoke attribute with a JSON result does not use this wrapping technique. Let's look at an example by changing our WCF service just a bit.
using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
[ServiceContract(Namespace = " ")]
public interface Service
{
[WebInvoke(Method="POST",ResponseFormat=WebMessageFormat.Json)]
[OperationContract]
string HelloWorld();
}
public class ServiceImpl : Service
{
public string HelloWorld()
{
return "Hello World";
}
}
We added the WebInvoke attribute to our service and explicitly specified the result to be encoded as JSON. Since we are using the WebInvoke attribute, we need to change our configuration slightly as well.
<system.serviceModel>
<services>
<service name="ServiceImpl">
<endpoint
address=""
binding="webHttpBinding"
contract="Service"
behaviorConfiguration="AjaxBehavior"/>
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior
name="AjaxBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
The only change here is that we changed the behavior from enableWebScript to webHttp, which enables us to control the projection of this service using the HTTP programming model.
If we invoke this service using a tool like Fiddler (using the same HTTP headers and URL mentioned above), the result looks like this:
HTTP/1.1 200 OK
Server: ASP.NET Development Server/9.0.0.0
Date: Wed, 09 Apr 2008 14:06:52 GMT
X-AspNet-Version: 2.0.50727
Cache-Control: private
Content-Type: application/json; charset=utf-8
Content-Length: 13
Connection: Close
"Hello World"
Note the obvious ommission of the "d" wrapper. This means that our service is now incompatible with our previously created JavaScript proxy, we'd have to write code to send a POST request and parse the result. Even after doing this, it becomes quite problematic to parse the JSON result using JavaScript (it's a string without an object name to reference... how do you parse that?). Hopefully it becomes immediately evident that using the first approach (enableWebScript behavior) provides a simpler and more secure programming model.
For More Information
- enableWebScript, UriTemplate, and HTTP Methods
- using a tool like Fiddler to create your own HTTP request
- Calling Web Services via AJAX - Part 1
- Calling Web Services via Client Script - MSDN
- ASP.NET AJAX Documentation - MSDN
- WebScriptEnablingBehavior - MSDN
- Creating WCF Services for ASP.NET AJAX - MSDN
Comments
Anonymous
April 09, 2008
PingBack from http://web-design.crazyblogz.info/?p=7209Anonymous
April 23, 2008
Hi, I contacted you over email, about this, and your original response was helpful in getting me to think of just how to go about with the project. If you don't recall I'm attempting to make a server-side AJAX (and hopefully WCF). The resources you sent me, should be good. But unfortunately I've hit a couple of snags. On everything the documentation of the WCF LOB Adapter SDK it refers to a tools folder that would contain all the tools I'd need for Visual Studio. I've installed a good 4 or 5 times and no such folder ever appears. That said, even if I get that done, is there any way at all for me to use the tools with Visual Studio 2008 Express Edition for Web Development? I appreciate your help Kirk, I really do, I'm just having troubles getting this thing started.Anonymous
May 02, 2008
Any suggestions on cross-domain service consumption? ie Service.set_path("http://different.domain.com/someOtherService.svc"); Thanks