Download JsonRestWCF.zip - 53.59 KB

Introduction

This article presents a method to consume cross-domain WCF Rest Services using jQuery Ajax Calls with an example.

Background

Representational State Transfer (REST) is a style of software architecture for distributed hypermedia systems such as the World Wide Web. It was first introduced by Roy Fielding in his PhD thesis "Architectural Styles and the Design of Network-based Software Architectures". You can find tutorials on creating WCF Rest services from here, here and here. Among these tutorials, I find "RESTful WCF Services with No svc file and No config" presented by Steve Michelotti particularly interesting. It is the simplest way to create a WCF rest service, and probably any type of service that I have ever found. Instead of focusing on the creation of WCF Rest services, this article will present a method to consume WCF Rest Services using jQuery Ajax Calls at the web browser side with an example.

SolutionExplorer.JPG

The attached Visual Studio solution has two projects:

  • The "StudentService" project is a WCF Rest service
  • The "StudentServiceTestWeb" is a simple "ASP.Net MVC" project, where I will show how to consume the service exposed by the "StudentService" project using "jQuery" "Ajax" calls from web browsers.

This Visual Studio solution is developed in Visual Studio 2010 and the jQuery version is "1.4.4". In this article, I will first introduce the WCF Rest service project "StudentService" and then introduce the web project "StudentServiceTestWeb". After that, I will talk about some "cross-domain" issues when consuming WCF Rest services using "jQuery" "Ajax" calls from web browsers.

The WCF Rest service project "StudentService"

The WCF Rest service is implemented in a simple "ASP.Net" web application. The method used to develop the example WCF Rest service is an exact copy of the method introduced by Steve Michelotti. If you take a look at his blog, it will make your understanding of this example much easier.

SolutionExplorerService.JPG

Before going to the WCF Rest service, Let us first take a look at the data model used by the service, which is implemented in the "Models\ModelClasses.cs" file:

using System;
using System.Collections.Generic;
 
namespace StudentService.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public Nullable<int> Score { get; set; }
        public string State { get; set; }
    }
 
    public class Department
    {
        public string SchoolName { get; set; }
        public string DepartmentName { get; set; }
        public List<Student> Students { get; set; }
    }
}

Two related classes "Student" and "Department" are implemented, where the "Department" class holds a reference to a "List" of "Student" objects. With these two classes as the data model, the WCF Rest service is implemented in the "Service.cs" file:

using System.Web;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using StudentService.Models;
using System.ServiceModel;
using System.Collections.Generic;
using System;
 
namespace StudentService
{
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode
        = AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service
    {
        [OperationContract]
        [WebGet(UriTemplate = "Service/GetDepartment")]
        public Department GetDepartment()
        {
            Department department = new Department
            {
                SchoolName = "School of Some Engineering",
                DepartmentName = "Department of Cool Rest WCF",
                Students = new List<Student>()
            };
 
            return department;
        }
 
        [OperationContract]
        [WebGet(UriTemplate = "Service/GetAStudent({id})")]
        public Student GetAStudent(string id)
        {
            Random rd = new Random();
            Student aStudent = new Student
                {
                    ID = System.Convert.ToInt16(id), 
                    Name = "Name No. " + id,
                    Score = Convert.ToInt16(60 + rd.NextDouble() * 40),
                    State = "GA"
                };
 
            return aStudent;
        }
 
        [OperationContract]
        [WebInvoke(Method ="POST", UriTemplate = "Service/AddStudentToDepartment",
            BodyStyle = WebMessageBodyStyle.WrappedRequest)]
        public Department
            AddStudentToDepartment(Department department, Student student)
        {
            List<Student> Students = department.Students;
            Students.Add(student);
 
            return department;
        }
    }
}

Three "OperationContracts" are implemented in this service:

  • "GetDepartment" accepts "GET" method. It takes no input parameter and returns a "Department" object. This "Department" object has an empty "Students" "List".
  • "GetAStudent" accepts "GET" method. It takes a single input parameter "id". It creates a "Student" object of the same "id", and returns this object to the caller.
  • "AddStudentToDepartment" accepts "POST" method, it takes two input parameters, a "Department" object and a "Student" object. It then adds the "Student" to the "Students" "List" of the "Department", and returns the "Department" object back to the caller.

To make this WCF Rest service work, we will need to make change to the "Global.asax.cs" file and the "Web.config" file. We will need to add the following code to the "Application_Start" event in the "Global.asax.cs":

protected void Application_Start(object sender, EventArgs e)
{
     RouteTable.Routes.Add(new ServiceRoute("", 
          new WebServiceHostFactory(), 
          typeof(Service)));
}

We will also need to add the following configuration into the "Web.config" file:

 <system.serviceModel>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
      multipleSiteBindingsEnabled="true" />
    <standardEndpoints>
      <webHttpEndpoint>
        <standardEndpoint name="" helpEnabled="true"
          automaticFormatSelectionEnabled="true" />
      </webHttpEndpoint>
    </standardEndpoints>
</system.serviceModel>

For those of you who are familiar with WCF service development, you may be surprised by the simplicity to develop and config a WCF Rest service. Yes, without a "svc" file and without the "Endpoint" configuration, the WCF Rest service is completed and works. The following picture shows the "help page" of this WCF Rest service:

ServiceHelp.JPG

The "Description" field has the "url" to access each "OperationContracts" method in the development environment in the "debug" mode.

The MVC web project "StudentServiceTestWeb"

To demonstrate how to consume the WCF Rest service using "jQuery" "Ajax" calls, an "ASP.Net MVC" application is developed.

SolutionExplorerWeb.JPG

This web application is a simplest "MVC" application possible. It has only one "controller" in the "Controllers\HomeController.cs" file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace StudentServiceTestWeb.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
    }
}

The only purpose of the "HomeController" is to load the web application's only "view" page "Index.aspx":

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Consume WCF Rest Service with jQuery</title>
    <link rel="stylesheet"
        href='<%= Url.Content("~/Content/Styles/Site.css") %>'
        type="text/css" />
</head>
<body>
    <div id="divContainer">
    <div id="divDepartmentHeader"></div>
    <div id="divStudentList"></div>
    <div id="divNewStudent">
        <span>Pending new student: - Click "Add student to department" button</span>
        <br />
        <span id="spanStudentList"></span>
    </div>
 
    <div id="divButtonContainer" style="float: right; margin: 2px">
        <button id="btnAquireNewStudent">Aquire a new student</button>
        <button id="btnAddStudentToDepartment">
            Add student to department</button>
    </div>
    </div>
</body>
</html>
 
<script src='<%= Url.Content("~/Content/Scripts/jquery-1.4.4.min.js") %>'
        type="text/javascript">
</script>
 
<script type="text/javascript">
    // Urls to access the WCF Rest service methods
    var GetDepartmentURl = "http://localhost:4305/Service/GetDepartment";
    var GetAStudent = "http://localhost:4305/Service/GetAStudent";
    var AddStudentToDepartment = "http://localhost:4305/Service/AddStudentToDepartment";
 
    var nextStudentID = 1;
    var jDepartment = null;
    var jPendingStudent = null;
 
    // Utility function - Create HTML table on Json data
    function makeTable(jObject) {
        var jArrayObject = jObject
        if (jObject.constructor != Array) {
            jArrayObject = new Array();
            jArrayObject[0] = jObject;
        }
 
        var table = document.createElement("table");
        table.setAttribute('cellpadding', '4px');
        table.setAttribute('rules', 'all');
        var tboby = document.createElement("tbody");
 
        var trh = document.createElement('tr');
        for (var key in jArrayObject[0]) {
            var th = document.createElement('th');
            th.appendChild(document.createTextNode(key));
            trh.appendChild(th);
        }
        tboby.appendChild(trh);
 
        $.each(jArrayObject, function (i, v) {
            var tr = document.createElement('tr');
            for (var key in v) {
                var td = document.createElement('td');
                td.appendChild(document.createTextNode(v[key]));
                tr.appendChild(td);
            }
            tboby.appendChild(tr);
        });
        
        table.appendChild(tboby)
 
        return table;
    }
 
    $(document).ready(function () {
        $.ajax({
            cache: false,
            type: "GET",
            async: false,
            dataType: "json",
            url: GetDepartmentURl,
            success: function (department) {
                jDepartment = department;
 
                var div = document.createElement("div");
                var span = document.createElement("span");
                span.setAttribute('id', 'DepartmentHeader');
                span.appendChild(document.createTextNode(department.SchoolName));
                span.appendChild(document.createTextNode(" - "));
                span.appendChild(document.createTextNode(department.DepartmentName));
                div.appendChild(span);
                span = document.createElement("span");
                div.appendChild(document.createElement("br"));
                span.appendChild(document.createTextNode("List of students:"));
                div.appendChild(span);
 
                $("#divDepartmentHeader").html("").append(div);
            },
            error: function (xhr) {
                alert(xhr.responseText);
            }
        });
 
        $("#btnAquireNewStudent").click(function () {
            if (jPendingStudent != null) {
                alert("Please add the pending student to the department first");
                return;
            }
 
            $.ajax({
                cache: false,
                type: "GET",
                async: false,
                url: GetAStudent + "(" + nextStudentID + ")",
                dataType: "json",
                success: function (student) {
                    jPendingStudent = student;
                    nextStudentID++;
 
                    var table = makeTable(student);
                    $("#spanStudentList").html("").append(table);
                    $("#divNewStudent").css("visibility", "visible");
                    $("#divNewStudent").show();
                },
                error: function (xhr) {
                    alert(xhr.responseText);
                }
            });
        });
 
        $("#btnAddStudentToDepartment").click(function () {
            if (jDepartment == null) {
                alert("Please wait until the department loads");
                return;
            }
            if (jPendingStudent == null) {
                alert("Please first get a new student to add to the department");
                return;
            }
 
            var jData = {};
            jData.department = jDepartment;
            jData.student = jPendingStudent;
 
            $.ajax({
                cache: false,
                type: "POST",
                async: false,
                url: AddStudentToDepartment,
                data: JSON.stringify(jData),
                contentType: "application/json",
                dataType: "json",
                success: function (department) {
                    jPendingStudent = null;
                    jDepartment = department;
 
                    var table = makeTable(department.Students);
                    $("#divStudentList").html("").append(table);
                    $("#divNewStudent").hide();
                },
                error: function (xhr) {
                    alert(xhr.responseText);
                }
            });
        });
    });
</script>

All the client side "javascript" code to access the WCF Rest service using "jQuery" "Ajax" calls is implemented in the "Index.aspx" page. Before going to the "javascript" section, let us first take a look at the html components:

  • There are a couple of "<div>" tags in the "<body>" tag. These "<div>" tags will be used as place holders by the "javascript" code to build the application's UI components.
  • A "button" control "btnAquireNewStudent". Clicking on this button will issue an "Ajax" call to the "OperationContracts" "GetAStudent".
  • A "button" control "btnAddStudentToDepartment". Clicking on this button will issue an "Ajax" call to the "OperationContracts" "AddStudentToDepartment".

The "javascript" code in the "Index.aspx" page is pretty simple:

  • In the "$(document).ready()" event, an "Ajax" call is made to the "GetDepartment" method in the WCF Rest service. Upon receiving the response from the server, the UI is updated by the information from the "Json" representation of the "Department" object. This "Json" object is then assigned to the global variable "jDepartment".
  • In the click event of the "btnAquireNewStudent", an "Ajax" call is made to the "GetAStudent" method. The global variable "nextStudentID" is used as the input parameter. If the call is successful, the function "makeTable" is used to create an html "table" using the received "Json" object and the UI is updated to show the newly received student. This "Json" object is then assigned to the global variable "jPendingStudent" and the global variable "nextStudentID" is adjusted, so a different "id" is sent when the user clicks this "button" again.
  • In the click event of the "btnAddStudentToDepartment", an "Ajax" call is made to the "AddStudentToDepartment" method. This call uses "POST" method. The data posted to the "AddStudentToDepartment" is a "Json" string, which is generated by wrapping the "Json" objects "jDepartment" and "jPendingStudent". If the call is successful, the function "makeTable" is called to generate an html "table" for the "List" of the "Student" objects and the UI is updated accordingly.

Now we finish this "simple" "StudentServiceTestWeb" project. But before we can test run it in the development environment, we have another issue to talk about.

The "cross-domain" issue

The "Cross-domain" or "Cross-site" HTTP requests are HTTP requests for resources from a different domain than the domain of the resource making the request. Generally speaking, the default behavior of the web browsers is to block the "Cross-domain" accesses if they feel that the types of accesses are "un-safe". From the web browsers' perspective, a domain is recognized by the "url". For example, when the browser loads an image from "http://domainb.foo:8080/image.jpg", the domain of the image file is identified by "http://domainb.foo:8080", which includes the "port number".

In this article, the Visual Studio assigned the port number "4305" to the WCF Rest service and port number "5187" to the web application to the debug servers to run the application. When an "Ajax" call is made to the WCF Rest service from the web application in the debug mode, it is considered "Cross-domain" by the web browsers, since the web application and the service have different "port numbers". To address this problem, we will need the WCF Rest service to explicitly tell the web browsers that the "Cross-domain" access is allowed. This can be done in the "Global.asax.cs" file in the "StudentService" project:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;
using System.ServiceModel.Activation;
using System.Web.Routing;
 
namespace StudentService
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.Add(new ServiceRoute("", new WebServiceHostFactory(), typeof(Service)));
        }
 
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
 
                HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                HttpContext.Current.Response.Cache.SetNoStore();
                
                EnableCrossDmainAjaxCall();
        }
 
        private void EnableCrossDmainAjaxCall()
        {
            HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin",
                          "http://localhost:5187");
 
            if (HttpContext.Current.Request.HttpMethod == "OPTIONS")
            {
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Methods", 
                              "GET, POST");
                HttpContext.Current.Response.AddHeader("Access-Control-Allow-Headers",
                              "Content-Type, Accept");
                HttpContext.Current.Response.AddHeader("Access-Control-Max-Age",
                              "1728000");
                HttpContext.Current.Response.End();
            }
        }
    }
}

In the "Application_BeginRequest" event, the function "EnableCrossDmainAjaxCall" is called. This function sends the required "HTTP access control" headers to the browsers. Among the popular browsers like "Firefox", "Chrome", "Safari", and "IE", each may behave slightly differently, but generally all follow the similar pattern.

  • For the "jQuery" "Ajax" calls to be successful, the browsers must receive the response header "Access-Control-Allow-Origin" and the value of this header must identify the domain of the calling web application. If you want all the web pages from all the domains to access your services, you can set the value as "*".
  • If the browsers feel that your "Ajax" call is "un-safe", they will send a "preflighted" request to the server, before they make the actual "Ajax" call. In this article, the call to the " OperationContract" "AddStudentToDepartment" uses "POST" methiod and the "content type" is "application/json", it is considered as "un-safe" by most browsers. The "preflighted" request uses "OPTIONS" method. If the server receives an "OPTIONS" request, it needs to use "Access-Control-Allow-Methods" to inform the browser the HTTP methods allowed for the "Ajax" call. It also needs to use "Access-Control-Allow-Headers" to inform the browser the HTTP headers allowed in the actual "Ajax" call. In this article, the WCF Rest service allows the browsers to use "POST" and "GET" method, and allows the headers "Content-Type" and "Accept" used in the "Ajax" call.
  • The value of the "Access-Control-Max-Age" tells the browsers how long the "preflighted" results remain valid, so the browsers issue the same "Ajax" call again they do not need to send the "OPTIONS" request before the "preflighted" results expire.

Different browsers may be slightly different with the "HTTP access control". But I have tested the "EnableCrossDmainAjaxCall" function with "Firefox", "Chrome", "Safari", and "IE". They all respond nicely to the headers sent by the "EnableCrossDmainAjaxCall" function.

Run the application

To run the application, you need to make sure that both the WCF Rest service and the web application are running. If you set the "StudentServiceTestWeb" project as the "start up" project, when you debug run the application, the Visual Studio should start both the service and the web application. When the web page first launches, the "OperationContract" "GetDepartment" is called and the school name and the department name loaded from the service are shown on the web browser:

ApplicationLaunch.JPG

Click on the "Aquire a new student" button, the "OperationContract" "GetAStudent" is called and the information for the new "Student" is displayed in the web browser:

ApplicationNewStudent.JPG

Click on the "Add student to department" button, the "OperationContract" "AddStudentToDepartment" is called and the UI is updated to show the "List" of the "Student" objects in the "Department". The following pictures shows the result after multiple "Student" objects added to the "Department":

ApplicationAddStudent.JPG

Points of Interest

  • This article presents a method to consume cross-domain WCF Rest Services using jQuery Ajax Calls with an example.
  • Using the method from Steve Michelotti's blog, a WCF Rest service can be easily implemented.
  • To call a WCF Rest service located at a different domain other than your web application, you need to be aware of the "HTTP access control" issue. I have tested the method presented in this article and it works on all the popular browsers.
  • When I develop the function "EnableCrossDmainAjaxCall" used in this article, I put a lot of effort to "allow" the "Cross-domain" "Ajax" calls. By studying the materials in the "HTTP access control" link, it should not be difficult to create a mechanism to "allow" the wanted "Cross-domain" accesses and "block" all others for better security.
  • Instead of using the method presented in this article, there are some discussions on using "JsonP" to solve the "Cross-domain" issues. If you are interested, you can take a look at it.
  • An interesting thing that I noticed during my own test is that the web application runs very fast in "IE" but significently slower in other browsers when the WCF Rest service is hosted in the Visual Studio debug server. I then deployed both the web application and the service in true IIS servers. When they are hosted by true IIS servers, there is no speed difference among the browsers, at least no human noticeable speed difference.
  • I hope you like my postings and I hope that they can help you one way or the other.

History

This is the first revision of this article.