Calling WCF Web Services from JavaScript
This post was long due, so here it is. Prepare for a long post!
Whenever you need to consume a WCF web service from a web page, you have (at least) three options:
- Have the ASP.NET ScriptManager generate a strongly-typed JavaScript proxy to the service that you can call directly (you even get Visual Studio intellisense!)
- Use your own JavaScript, or some third party, library such as jQuery (which I use in my example) to invoke a service in REST style
- Use your own JavaScript to invoke a service using SOAP
The first two require that you have control over the bindings specified in the Web.config file or, at least, the factory in the .svc file.
We want to be able to invoke a service looking like this:
public class Response
{
public String A { get; set; }
public String B { get; set; }
}
public Response PostTest(String a, String b);
public Response GetTest(String a, String b);
You probably know the difference between SOAP and REST, if not, check out this and this.
Let's start from the first option.
.NET 3.5 included a handy behavior, enableWebScript, which allows the ScriptManager to generate a proxy from the metadata published from the service. The service must look like this:
namespace WcfAjax.Services
{
[DataContract]
public class Response
{
[DataMember]
public String A { get; set; }
[DataMember]
public String B { get; set; }
}
[ServiceContract(Namespace = "WcfAjaxServices")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class TestService
{
[OperationContract]
[WebInvoke]
public Response PostTest(String a, String b)
{
return (new Response() { A = a, B = b });
}
[OperationContract]
[WebGet]
public Response GetTest(String a, String b)
{
return (new Response() { A = a, B = b });
}
}
}
Please note the following:
- WebInvoke and WebGet attributes placed each of the operation (web) methods (those marked with OperationContract)
- AspNetCompatibilityRequirements attribute
- The WcfAjaxServices namespace specified in the ServiceContract
WebGetand WebInvoke allow us to specify wether we want to call the method using the HTTP GET method or the POST, respectively. This may be relevant if you want to send sensitive information, such as passwords, in which case you should use the POST method (the default if no attribute is specified); on the other hand, only GET requests can be cached.
By now you should probably know what the AspNetCompatibilityRequirements is, if not, go read the documentation.
As for the namespace, make sure you do not use something that looks like a URL, make it simple, you will see why.
You now need to register the service with the ScriptManager on the page (or master page) that you want to call the service in. Here's how we do it:
<asp:ScriptManager runat="server"> <Services> <asp:ServiceReference Path="~/Services/TestService.svc"/> </Services> </asp:ScriptManager>
Make sure the WCF bindings are configured this way in Web.config:
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="WcfAjax.Services.TestService">
<enableWebScript />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="WcfAjax.Services.TestService">
<endpoint address="" behaviorConfiguration="WcfAjax.Services.TestService" binding="webHttpBinding" contract="WcfAjax.Services.TestService" />
</service>
</services>
</system.serviceModel>
The .svc file contains:
<%@ ServiceHost Language="C#" Debug="true" Service="WcfAjax.Services.TestService" CodeBehind="TestService.svc.cs" %>
Technically, you can go without using enableWebScript, if you specify a special factory in the .svc file:
<%@ ServiceHost Language="C#" Debug="true" Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory" Service="WcfAjax.Services.TestService" CodeBehind="TestService.svc.cs" %>
And that's it. You can now call the service in JavaScript:
<script type="text/javascript">
//<![CDATA[
function test()
{
//the class name is named from the namespace specified in the OperationContract attribute
//plus the contract class name
var svc = new WcfAjaxServices.TestService();
//the methods look like the ones defined in the contract, but they take 3 additional arguments:
//- a function to call in case of success
//- a function to call in case of error
//- an optional context
//result and error are JavaScript objects
//methodName is the name of the function that started the request
svc.GetTest('a', 'b', function(result, context, functionName)
{
window.alert('A: ' + result.A);
}, function (error, context, methodName)
{
window.alert('error: ' + error);
}, null);
svc.PostTest('a', 'b', function(result, context, functionName)
{
window.alert('A: ' + result.A);
}, function (error, context, methodName)
{
window.alert('error: ' + error);
}, null);
}
//]]>
</script>
Moving on, the next option gives us total control over the way our parameters are sent to the service. Unfortunately, we cannot rely on automatically generated proxies, but it's easy anyway. This one relies on the webHttp behavior, which is also new on .NET 3.5. Specifically, this behavior allows REST-style calls.
Have your code look like this:
namespace WcfAjax.Services
{
[ServiceContract(Namespace = "WcfAjaxServices")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class RestTestService
{
[OperationContract]
[WebInvoke(BodyStyle = WebMessageBodyStyle.Wrapped, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
public Response PostTest(String a, String b)
{
return (new Response() { A = a, B = b });
}
[OperationContract]
[WebGet(UriTemplate = "GetTest?a={a}&b={b}", ResponseFormat = WebMessageFormat.Json)]
public Response GetTest(String a, String b)
{
return (new Response() { A = a, B = b });
}
}
}
The configuration should look like this:
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="WcfAjax.Services.RestTestService">
<webHttp />
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service name="WcfAjax.Services.RestTestService">
<endpoint address="" behaviorConfiguration="WcfAjax.Services.RestTestService" binding="webHttpBinding" contract="WcfAjax.Services.TestService" />
</service>
</services>
</system.serviceModel>
As for the .svc file, nothing new:
<%@ ServiceHost Language="C#" Debug="true" Service="WcfAjax.Services.RestTestService" CodeBehind="RestTestService.svc.cs" %>
Now, the difference is in the way we invoke the service. Here's an example using jQuery AJAX:
<script type="text/javascript">
//<![CDATA[
function restGetTest()
{
$.ajax
(
{
type: 'GET',
url: 'Services/RestTestService.svc/GetTest',
dataType: 'json',
data: 'a=a&b=b',
success: function (response, type, xhr)
{
window.alert('A: ' + response.A);
},
error: function (xhr)
{
window.alert('error: ' + xhr.statusText);
}
}
);
$.ajax
(
{
type: 'POST',
url: 'Services/RestTestService.svc/PostTest',
dataType: 'json',
contentType: 'application/json',
data: '{ "a": "a", "b": "b" }',
success: function (response, type, xhr)
{
window.alert('A: ' + response.PostTestResult.A);
},
error: function (xhr)
{
window.alert('error: ' + xhr.statusText);
}
}
);
}
//]]>
</script>
Of note:
- The data for the GET version must match the format specified in the WebGet attribute, by the UriTemplate property
- For the POST version, the data object is a JSON-formatted string
Finally, some times we do not have possibility to change either the bindings or the factory that is used to create the services, and we have to deal with plain SOAP. Luckily, although SOAP can get quite complex, for simple scenarios it isn't too hard to handle.
The code:
namespace WcfAjax.Services
{
[ServiceContract(Namespace = "WcfAjaxServices")]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class SoapTestService
{
[OperationContract]
public Response PostTest(String a, String b)
{
return (new Response() { A = a, B = b });
}
[OperationContract]
public Response GetTest(String a, String b)
{
return (new Response() { A = a, B = b });
}
}
}
Note this looks exactly like an ordinary WCF service, that doesn't even need ASP.NET compatibility mode. The configuration:
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="WcfAjax.Services.SoapTestService"/>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
<services>
<service name="WcfAjax.Services.SoapTestService">
<endpoint address="" behaviorConfiguration="WcfAjax.Services.SoapTestService" binding="basicHttpBinding" contract="WcfAjax.Services.SoapTestService" />
</service>
</services>
</system.serviceModel>
Unlike the other examples, we must use the basicHttpBinding, not , for this one.
The .svc file:
<%@ ServiceHost Language="C#" Debug="true" Service="WcfAjax.Services.SoapTestService" CodeBehind="SoapTestService.svc.cs" %>
Finally, the invocation (SOAP only allows POST):
<script type="text/javascript">
//<![CDATA[
function test()
{
$.ajax
(
{
type: 'POST',
url: 'Services/SoapTestService.svc',
dataType: 'xml',
contentType: 'text/xml',
data: '' +
//uncomment this if you want to send a custom SOAP header
//'' +
//'' +
//'MyName ' +
//'MyPassword ' +
//' ' +
//' '
'' +
'' +
'a' +
'b' +
' ' +
' ' +
' ',
beforeSend: function (xhr)
{
xhr.setRequestHeader('SOAPAction', 'WcfAjaxServices/SoapTestService/PostTest');
},
success: function (response, type, xhr)
{
var a = '';
if (response.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].textContent)
{
//Chrome and Firefox
a = response.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].textContent;
}
else if (response.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].text)
{
//IE
a = response.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].text;
}
window.alert('A: ' + a);
},
error: function (xhr)
{
window.alert('error: ' + xhr.statusText);
}
}
);
}
//]]>
</script>
And here you have it. Hope this turns out useful to anyone! If you need the code, drop me a line.