RESTful WCF Services

28 August 2011

Representational State Transfer (REST) is an architectural style for developing distributed computing systems. Unlike SOAP-based web services, which focus on encapsulating business processes, REST-based web services encapsulate data, which clients then manipulate with their own business logic.

It should be noted that REST is an architecture style, while SOAP is a well-defined protocol.

Both REST and SOAP are equally valid, and both have their advantages and disadvantages: REST is simpler and more lightweight in terms of network payload, while SOAP provides greater flexibility and enhancements such as state management, security, transactions, etc.

REST is becoming increasingly popular due to the ease with which JavaScript clients can consume REST services which return JSON (JavaScript Object Notation) data.

REST makes use of simple HTTP verbs such as GET, POST, PUT, DELETE that are mapped to particular URIs to perform semantically appropriate operations. For example, accessing the http://localhost/customers URI will result in a different operation, depending on the HTTP verb used for the request:

  • GET – returns a list of all customers
  • POST – create a new customer
  • PUT – update an existing customer
  • DELETE – delete a customer

WCF provides excellent support for the creation and consumption of REST-based services. In addition, the Microsoft Entity Framework offers WCF Data Services (formerly known as ADO.NET Data Services) which facilitate the exposure of data from an entity model as REST services. WCF Data Services are the Microsoft .NET implementation of the Open Data Protocol (OData) protocol, which is built around web standards such HTTP, JSON, etc.

Creating a WCF REST Service

  1. Create a new solution containing a .NET Console project (named WcfRestService) using Visual Studio 2010
  2. In Solution Explorer, right-click the new project and select Add New Item
  3. Select WCF Service from the list of templates and name the service RestService.cs
  4. We now need to add a reference to the System.ServiceModel.Web.dll assembly, as this contains important support for REST-based WCF services.

    However, you will not be able to add a reference to this assembly until you change the project's target framework from the .NET Client Profile 4 (which includes only a limited number of namespaces) to the full .NET Framework 4
  5. To make the required change, right-click the project in Solution Explorer and select Properties
  6. On the Application tab, change the target framework to .NET Framework 4:
  7. Now add the reference to System.ServiceModel.Web:
  8. Open the service's interface file and define IRestService as follows:
  9. using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.ServiceModel;
    using System.ServiceModel.Web;  
    using System.ServiceModel.Activation;
    using System.Text;
    using System.ComponentModel;
    
    namespace WcfRestService
    {
        [ServiceContract]
        public interface IRestService
        {
            [OperationContract]
            [Description("Get a list of MyRESTData objects")]
            [WebGet(UriTemplate = "Data", ResponseFormat = WebMessageFormat.Json)]  
            List<MyRESTData> GetData();
    
            [OperationContract]
            [Description("Get a particular MyRESTData object")]
            [WebGet(UriTemplate = "Data/{idStr}", 
              ResponseFormat = WebMessageFormat.Json)]
            MyRESTData GetDataID(string idStr);  
            // UriTemplate path variables must be strings
    
            [OperationContract]
            [Description("Add a MyRESTData object")]
            [WebInvoke(Method = "POST", 
              UriTemplate = "SetData?id={id}&name={name}", 
              ResponseFormat = WebMessageFormat.Json)]
            List<MyRESTData> SetData(int id, string name);  
            // UriTemplate query string vars can be non-string
        }
    
        [DataContract]
        public class MyRESTData
        {
            public MyRESTData() : this(0, "") { }
            public MyRESTData(int id, string name)
            {
                this.ID = id;
                this.Name = name;
            }
    
            [DataMember]
            public int ID;
    
            [DataMember]
            public string Name;
        }
    }

  10. There are several things to note about the above interface:
    • Our service will provide three methods, accessible via the following URIs:

      http://localhost/WcfRestService/Data
      http://localhost/WcfRestService/Data/IDSTR
      http://localhost/WcfRestService/SetData?id=ID&name=NAME
    • The [DataContract] attribute is used to mark any custom data type (e.g. in this instance our MyRESTData class) for serialization so that it may be exposed by WCF services and consumed by clients. The [DataMember] attribute marks individual members for serialization
    • The [WebGet] attribute marks a method as a logical retrieval operation that can be called by a REST client (using an HTTP GET)
    • The [WebInvoke] attribute marks a method as a logical change operation (e.g. POST, PUT, etc.) that can be called by a REST client (e.g. an HTTP POST, etc.)
    • The [Description] attribute allows you to create a simple auto-generated help page that describes the operations supported by the service (see later)
    • The UriTemplate allows you to control how requests get handled by services
    • Setting the ResponseFormat (e.g. to WebMessageFormat.Json) allows you to control the format of data returned by the service
    • Notice that GetData() and SetData() both return a complex type (List<MyRESTData>). You'll see later how Visual Studio can help us configure how such types are dealt with
    • All path variables (e.g. Data/{idStr}) specified in a UriTemplate must be strings. You can specify non-string parameters if they are part of a UriTemplate query string (e.g. SetData?id={id}&name={name})

  11. Now create the implementation of IService as follows:
  12. using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.ServiceModel;
    using System.ServiceModel.Activation;
    using System.Text;
    
    namespace WcfRestService
    {
        public class RestService : IRestService
        {
            private List<MyRESTData> m_data;
    
            public RestService()
            {
                m_data = new List<MyRESTData>();
                m_data.Add(new MyRESTData(0, "Fred Smith"));
                m_data.Add(new MyRESTData(1, "Jim Green"));
                m_data.Add(new MyRESTData(2, "Mary Brown"));
                m_data.Add(new MyRESTData(3, "Linda Black"));
                m_data.Add(new MyRESTData(4, "Sally White"));
            }
    
            public List<MyRESTData> GetData()
            {
                return m_data;
            }
    
            public MyRESTData GetDataID(string idStr)
            {
                return m_data[int.Parse(idStr)];
            }
    
            public List<MyRESTData> SetData(int id, string name)
            {
                m_data.Add(new MyRESTData(id, name));
                return m_data;
            }
        }
    }
    

  13. Implement self-hosting of the service in the project's Program.cs as follows:
  14. using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ServiceModel;
    using System.ServiceModel.Description;
    
    namespace WcfRestService
    {
        class Program
        {
            static void Main(string[] args)
            {
                using (ServiceHost host = 
                  new ServiceHost(typeof(WcfRestService.RestService)))
                {
                    host.Open();
                    Console.WriteLine("RestService is running...");
                    Console.ReadLine();
                    host.Close();
                }
            }
        }
    }
    

  15. Amend the project's app.config file as follows
  16. <?xml version="1.0"?>
    <configuration>
        <system.serviceModel>
            <behaviors>
                <endpointBehaviors>
                    <behavior name="REST">
                        <webHttp helpEnabled="true" />
                    </behavior>
                </endpointBehaviors>
                <serviceBehaviors>
                    <behavior name="">
                        <serviceMetadata httpGetEnabled="true" />
                        <serviceDebug includeExceptionDetailInFaults="true" />
                    </behavior>
                </serviceBehaviors>
            </behaviors>
            <bindings>
              <webHttpBinding>
                <binding name="webHttpBindingWithJsonP" 
                  crossDomainScriptAccessEnabled="true" />
              </webHttpBinding>
            </bindings>
          <services>
                <service name="WcfRestService.RestService">
                    <endpoint address=""
                              behaviorConfiguration="REST"
                              bindingConfiguration="webHttpBindingWithJsonP"
                              binding="webHttpBinding"
                              contract="WcfRestService.IRestService" />
                    <endpoint address="mex" binding="mexHttpBinding" 
                                            contract="IMetadataExchange" />
                    <host>
                        <baseAddresses>
                            <add baseAddress="http://localhost/WcfRestService" />
                        </baseAddresses>
                    </host>
                </service>
            </services>
        </system.serviceModel>
    </configuration>
    

  17. Important points to note about the configuration are:
    • We defined an endpoint behaviour named REST that turns on the service's auto-generate help page (see later)
    • In the binding configuration named webHttpBindingWithJsonP we enable cross-domain access to our service (the default is to disallow it)
    • The service's enpoint specifies the behavior and binding configurations.
      We also specify a binding of type webHttpBinding - this is what's required to configure a WCF service for REST-based operation
  18. Build and run the project
  19. Open a browser and navigate to the base address of the service at http://localhost/WcfRestService
  20. You should see:
  21. Now navigate to http://localhost/WcfRestService/help
  22. You should see the auto-generated help page:
  23. You can also test the Data and Data/{idStr} operations as follows (note that data is indeed returned in JSON format):
  24. You cannot directly test the SetData operation because it requires the browser to use a POST verb (it will actually use an HTTP GET) - if you try you’ll get something like the following:
  25. To prove to yourself what's going on, download a copy of Fiddler2 (if you don't already have this wonderful (free) web and network development debugging tool!) and run it
  26. Access http://localhost/WcfRestService/Data and watch what happens in Fiddler:
  27. Select the Inspectors tab and then select the Request Headers tab. The Response Raw tab shows the data returned. Notice how the request header clearly shows the HTTP command used to retrieve the data: GET /WcfRestService/Data HTTP/1.1
  28. Now try accessing the following URI to insert some data:

    http://localhost/WcfRestService/SetData?id=99&name=test:

  29. You should see in Fiddler something like the following:
  30. You can clearly see that an HTTP GET was used and that the response from our server was a 405 error (method not allowed). We can also see that the response denotes that only a POST is allowed (because that's what we configured in the operation's [WebInvoke] UriTemplate)
  31. We can use Fiddler to manually construct the required POST request.

    Select the Request Builder tab and then drag the previous (failed) GET request into the Request Builder tab:
  32. Edit the request. Set the HTTP verb to POST and add a content-length (of zero) parameter to the header as shown. Now click the Execute button
  33. You should see that the operation is carried out and the required data inserted (note the new entry at the end of the list in the JSON tab):

 

Creating a Console REST Client

Creating a .NET client for our service is reasonably straightforward. However, you cannot use the normal Visual Studio Add Service Reference proxy-generation procedure - that is only for use with WCF/SOAP-based services.

The approach taken is to construct a request programmatically using an HttpWebRequest object, while the response is handled by HttpWebResponse and StreamReader objects.

The following code shows all that's required to implement a simple console-based REST client app:

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Text;
  using System.Net;
  using System.IO;
  
  namespace WcfRestClient
  {
      class Program
      {
          static void Main(string[] args)
          {
              string data;
              StreamReader reader;
              Uri serviceUri;
  
              // *** Use HTTP GET to call the Data operation ***
              // ***********************************************
  
              serviceUri = new Uri("http://localhost/WcfRestService/Data");
              HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(serviceUri);
              request.Method = "GET"; // HTTP verb
              request.ContentType = "application/json; charset=utf-8";
  
              HttpWebResponse response = (HttpWebResponse)request.GetResponse();
              using (reader = new StreamReader(response.GetResponseStream()))
              {
                  data = reader.ReadToEnd();
              }
  
              Console.WriteLine("Response from GET (Data) service (" + 
                serviceUri.ToString() + "):");
              Console.WriteLine("  " + data);
  
              // *** Use HTTP POST to call the SetData operation ***
              // ***************************************************
              
              serviceUri = new Uri("http://localhost/WcfRestService/SetData");
              request = (HttpWebRequest)HttpWebRequest.Create(serviceUri);
              request.Method = "POST"; // HTTP verb
              request.ContentLength = 0;
              response = (HttpWebResponse)request.GetResponse();
              using (reader = new StreamReader(response.GetResponseStream()))
              {
                  data = reader.ReadToEnd();
              }
  
              Console.WriteLine("Response from POST (SetData) service (" + 
                serviceUri.ToString() + "):");
              Console.WriteLine("  " + data);
              Console.ReadLine();
          }
      }
  }
  

Creating a jQuery Client

Consuming REST-based services from JavaScript (here we use jQuery (version 1.5.1) to simplify the necessary AJAX calls) is very straightforward.

In this example we create a simple HTML/JavaScript client with the following interface:

The code shown below makes use of the jQuery $.ajax() method to call our WCF service's operations. As an example, let's take the code snippet that calls the http://localhost/WcfRestService/Data operation.

  $(function () {
      // Get a list of all data items
      $("#listBtn").click(function () {
          $.ajax(
          {
              type: 'GET',
              url: "http://localhost/WcfRestService/Data",
              cache: false,
              dataType: 'json',
              success: function (data) {
                  ListResults(data);
              },
              error: function (XMLHttpRequest, textStatus, errorThrown) {
                  $("#out").html("Error: " + errorThrown);
              }
          });
      });

The $(function () { ... }); code is called when the page has finished loading and the jQuery object is ready for use. We hook into the click event for the List Data button. When the handler is invoked we simply call the required URI using $.ajax(). We set the type parameter to an HTTP GET and specify a function to be called when the call succeeds. This function receives the output from the operation in its data parameter.

Here’s the example HTML/JavaScript code in full:

  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>jQuery REST Client</title>
  
  <script language='javascript' type='text/javascript' src='_scripts/jquery-1.6.2.min.js'></script>
  <script type="text/javascript">
  
      $(function () {
          // Get a list of all data items
          $("#listBtn").click(function () {
              $.ajax(
              {
                  type: 'GET',
                  url: "http://localhost/WcfRestService/Data",
                  cache: false,
                  dataType: 'json',
                  success: function (data) {
                      ListResults(data);
                  },
                  error: function (XMLHttpRequest, textStatus, errorThrown) {
                      $("#out").html("Error: " + errorThrown);
                  }
              });
          });
  
          $("#idBtn").click(function () {
              // Get a particular data item based on ID
              $.ajax(
              {
                  type: 'GET',
                  url: "http://localhost/WcfRestService/Data/" + $("#getID").val(),
                  cache: false,
                  dataType: 'json',
                  success: function (data) {
                      $("#out").html("ID: " + data.ID.toString() + "<br />");
                      $("#out").append("Name: " + data.Name + "<p />");
                  },
                  error: function (XMLHttpRequest, textStatus, errorThrown) {
                      $("#out").html("Error: " + errorThrown);
                  }
              });
          });
  
          $("#addBtn").click(function () {
              // Create a new data item
              var jsonData = {};
              jsonData.id = 99;
              jsonData.name = "Test";
  
              $.ajax(
              {
                  type: 'POST',
                  url: "http://localhost/WcfRestService/SetData?id=" + jsonData.id + "&name=" + jsonData.name,
                  cache: false,
                  dataType: 'json',
                  success: function (data) {
                      ListResults(data);
                  },
                  error: function (XMLHttpRequest, textStatus, errorThrown) {
                      $("#out").html("Error: " + errorThrown);
                  }
              });
          });
      });
  
      function ListResults(data) {
          $("#out").html("Results...<p></p>");
          for (var index in data) {
              $("#out").append("ID: " + data[index].ID.toString() + "<br />");
              $("#out").append("Name: " + data[index].Name + "<p />");
          }
      }
  </script>
  </head>
  
  <body>
  <p>jQuery REST Service Client</p>
  
  <p></p>
  <input id="addBtn" type="button" value="Add Data" />
  <input id="listBtn" type="button" value="List Data" />
  <p></p>
  <input id="getID" type="text" value="1" />
  <input id="idBtn" type="button" value="Get Data for index" />
  
  <div id="out"></div>
  
  </body>
  </html>
  

Cross-Domain Considerations

There are a couple of cross-domain issues that need to be considered when using jQuery to consume REST-based services. First, assume for the sake of argument that our service is running on:

http://myserver/myservice

So, the service is running on port 80 (the default) in the myserver domain.

It should be noted that in this context a request from a page hosted at any of the following would be considered cross-domain:

  • http://myserver:9001/mypage.html (different port)
  • http://myhost/mysite/mypage.html (different domain)
  • etc.

Thus, the service and the client must be running on the same server (or, more correctly, domain, in the case of a web farm) and the same port, otherwise the call is considered cross-domain.

The following points summarize the situation:

  • When a cross-domain call is made, the service must be configured to handle it using an endpoint binding configuration with crossDomainScriptAccessEnabled="true" (see the earlier example)
  • With jQuery's $.ajax() method, cross-domain requests will not succeed: no data will be available (the data object received by the $.ajax() success handler function will be null), even though Fiddler shows the data has actually been returned by the service, the browser will not make the data available
  • There is a workaround to the previous point, but only when using an HTTP GET: simply substitute the jsonp dataType for json in the $.ajax() call:
    $.ajax(
    {
        type: 'GET',
        url: "http://localhost/WcfRestService/Data",
        cache: false,
        dataType: 'jsonp',
        :
    
  • The jsonp workaround doesn't work when using an HTTP POST with $.ajax().
    If you try and do a cross-domain POST, $.ajax() will automatically covert the POST to an HTTP GET.

    This is documented and as-designed behavior, and you can see the GET-for-POST substitution happen in Fiddler. The issue will mainifest itself as a 405 server error (method not allowed) response from the server