[Dev tip] Converting SOAP-based WCF Service to RESTful Design

Introduction

When SOAP is believed to be overkill, some developers may choose RESTful services. REST (Representative State Transfer) is mostly used when developers need high range of interoperability and when trying to restrict themselves to basic XML messages or JSON that are transmitted over HTTP. RESTful services are fundamentally different from SOAP-based services because they don’t attempt to achieve transport neutrality. In fact, RESTful services typically embrace HTTP as the only transport used throughout the system. Using REST, developers can model their services as resources and give these resources unique identifiers in the form of URIs. In this article, we will take an existing SOAP-based service and convert it over to a more RESTful design.

Background

The Existing WCF SOAP Based Service

SOAP is developed on a great deal of work that’s been happening throughout the industry to implement a completely new protocol stack for services. It means that additional features or capabilities that we want to implement for our services should be possible to implement in a transport neutral way. And we’ll accomplish that in this protocol stack by using an XML-based messaging layer. Now, this is where SOAP comes into the picture. SOAP is a particular XML vocabulary for packaging up messages that we need to transmit to our services. The following is the code for the WCF based service before it’s converted to a RESTful service. The backend is using Entity Framework utilizing the Northwind database. Then it’s followed by an image, which is the sample result of the GetAllProducts, showing the SOAP request and response.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace SoapToRESTfullDemo
{
    [ServiceContract]
    public interface IProductService
    {
        [OperationContract]
        List<ProductEntity> GetAllProducts();
        [OperationContract]
        ProductEntity GetProductByID(int productID);
        [OperationContract]
        bool UpdateProduct(ProductEntity product);
    }

    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
    public class ProductService : IProductService
    {
        #region IProductService Members
        public List<ProductEntity> GetAllProducts()
        {
            List<ProductEntity> products = new List<ProductEntity>();
            ProductEntity productEnt = null;
            using (var NWEntities = new NorthwindEntities())
            {
                List<Product> prods = (from p in NWEntities.Products
                                 select p).ToList();
                if (prods != null)
                {
                    foreach (Product p in prods)
                    {
                        productEnt = new ProductEntity()
                        {
                            ProductID = p.ProductID,
                            ProductName = p.ProductName,
                            QuantityPerUnit = p.QuantityPerUnit,
                            UnitPrice = (decimal)p.UnitPrice,
                            UnitsInStock = (int)p.UnitsInStock,
                            ReorderLevel = (int)p.ReorderLevel,
                            UnitsOnOrder = (int)p.UnitsOnOrder,
                            Discontinued = p.Discontinued
                        };
                        products.Add(productEnt);
                    }
                }
            }
            return products;
        }

        public ProductEntity GetProductByID(int productID)
        {
                ProductEntity productEnt = null;
                using (var NWEntities = new NorthwindEntities())
                {
                    Product prod = (from p in NWEntities.Products
                                       where p.ProductID == productID
                                       select p).FirstOrDefault();
                    if (prod != null)
                        productEnt = new ProductEntity()
                        {
                            ProductID = prod.ProductID,
                            ProductName = prod.ProductName,
                            QuantityPerUnit = prod.QuantityPerUnit,
                            UnitPrice = (decimal)prod.UnitPrice,
                            UnitsInStock = (int)prod.UnitsInStock,
                            ReorderLevel = (int)prod.ReorderLevel,
                            UnitsOnOrder = (int)prod.UnitsOnOrder,
                            Discontinued = prod.Discontinued
                        };
                }
                return productEnt;
        }
        public bool UpdateProduct(ProductEntity product)
        {
            bool updated = true;
            using (var NWEntities = new NorthwindEntities())
            {
                var productID = product.ProductID;
                Product productInDB = (from p in NWEntities.Products
                                       where p.ProductID == productID
                                       select p).FirstOrDefault();
                if (productInDB == null)
                {
                    throw new Exception("No product with ID " + product.ProductID);
                }
                NWEntities.Products.Remove(productInDB);
                productInDB.ProductName = product.ProductName;
                productInDB.QuantityPerUnit = product.QuantityPerUnit;
                productInDB.UnitPrice = product.UnitPrice;
                productInDB.Discontinued = product.Discontinued;
                NWEntities.Products.Attach(productInDB);
                NWEntities.Entry(productInDB).State = System.Data.EntityState.Modified;
                int num = NWEntities.SaveChanges();
                if (num != 1)
                {
                    updated = false;
                }
            }
            return updated;
        }
        #endregion
    }
}

Using the Code

The Conversion

The interaction of REST is done through a standard uniform interface or service contract. In this case, it would be the methods defined by the HTTP protocol specifically GET, POST, PUT, and DELETE. By standardizing on the uniform interface, developers can build infrastructure around the semantic meaning of each operation and make performance and scalability improvements when possible. For security, REST simply uses HTTPS; it just leverages SSL for all its security needs.

We will start with the three operations: GetAllProducts, which returns all products, GetProductByID where we provide a productID for the product that we are looking for, and finally, UpdateProduct will demonstrate the WebInvoke operation which is passing the PUT method.

First, we need to add the ServiceModel.Web assembly which gives us access to the WebGet and WebInvoke methods. The following is the step by step instruction for converting the IProduct interface:

  1. In the Product interface, let’s define our URI mapping which specifies what URI to map to; for instance,[WebGet(UriTemplate = "products")] for the GetAllProducts method
  2. For the GetProductByID, we will need to pass in the base address, followed by the product, then followed by the productID – [WebGet(UriTemplate = "product/{productID}")]
  3. WebInvoke uses the same property. The update/submit method uses the POST method; for instance,[WebInvoke(Method = "POST", UriTemplate = "product")]

Your complete code should look like the following (as you can see, the difference with SOAP-based is just around the interface):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.ServiceModel.Syndication;

namespace SoapToRESTfullDemo
{
    [ServiceContract]
    public interface IProductService
    {
        [WebGet(UriTemplate = "products")]
        [OperationContract]
        List<ProductEntity> GetAllProducts();

        //UriTemplate - the base address, followed by product and followed by the ID
        [WebGet(UriTemplate = "product/{productID}")] 
        [OperationContract]
        ProductEntity GetProductByID(string productID);
                
        //WebInvoke has the same property - for update/submit use POST. Post it to product
        [WebInvoke(Method = "POST", UriTemplate = "product")]
        [OperationContract]
        bool UpdateProduct(ProductEntity product);
    }

    [ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
    public class ProductService : IProductService
    {
        #region IProductService Members
        public List<ProductEntity> GetAllProducts()
        {
            List<ProductEntity> products = new List<ProductEntity>();
            ProductEntity productEnt = null;
            using (var NWEntities = new NorthwindEntities())
            {
                List<Product> prods = (from p in NWEntities.Products
                                 select p).ToList();
                if (prods != null)
                {
                    foreach (Product p in prods)
                    {
                        productEnt = new ProductEntity()
                        {
                            ProductID = p.ProductID,
                            ProductName = p.ProductName,
                            QuantityPerUnit = p.QuantityPerUnit,
                            UnitPrice = (decimal)p.UnitPrice,
                            UnitsInStock = (int)p.UnitsInStock,
                            ReorderLevel = (int)p.ReorderLevel,
                            UnitsOnOrder = (int)p.UnitsOnOrder,
                            Discontinued = p.Discontinued
                        };
                        products.Add(productEnt);
                    }
                }
            }
            return products;
        }

        public ProductEntity GetProductByID(string productID)
        {
            int pID = Convert.ToInt32(productID);
            ProductEntity productEnt = null;
            using (var NWEntities = new NorthwindEntities())
            {
                Product prod = (from p in NWEntities.Products
                                where p.ProductID == pID
                                select p).FirstOrDefault();
                if (prod != null)
                    productEnt = new ProductEntity()
                    {
                        ProductID = prod.ProductID,
                        ProductName = prod.ProductName,
                        QuantityPerUnit = prod.QuantityPerUnit,
                        UnitPrice = (decimal)prod.UnitPrice,
                        UnitsInStock = (int)prod.UnitsInStock,
                        ReorderLevel = (int)prod.ReorderLevel,
                        UnitsOnOrder = (int)prod.UnitsOnOrder,
                        Discontinued = prod.Discontinued
                    };
            }
            return productEnt;
        }
        public bool UpdateProduct(ProductEntity product)
        {
            bool updated = true;
            using (var NWEntities = new NorthwindEntities())
            {
                var productID = product.ProductID;
                Product productInDB = (from p in NWEntities.Products
                                       where p.ProductID == productID
                                       select p).FirstOrDefault();
                if (productInDB == null)
                {
                    throw new Exception("No product with ID " + product.ProductID);
                }
                NWEntities.Products.Remove(productInDB);
                productInDB.ProductName = product.ProductName;
                productInDB.QuantityPerUnit = product.QuantityPerUnit;
                productInDB.UnitPrice = product.UnitPrice;
                productInDB.Discontinued = product.Discontinued;
                NWEntities.Products.Attach(productInDB);
                NWEntities.Entry(productInDB).State = System.Data.EntityState.Modified;
                int num = NWEntities.SaveChanges();
                if (num != 1)
                {
                    updated = false;
                }
            }
            return updated;
        }
        #endregion
    }
}

Once we have the interface modified, we can then modify the configuration file (app.config) to wire up the service. The following are the steps for modifying the app.config:

  1. Change binding from basic to WebHttpBinding – <endpoint address ="" binding="wsHttpBinding" contract="SoapToRESTfullDemo.IProductService">
  2. Add a new behavior – <behavior name="SoapToRESTfullDemo.Service1Behavior">
  3. Apply this behavior to the service – <service name="SoapToRESTfullDemo.ProductService" behaviorConfiguration="SoapToRESTfullDemo.Service1Behavior">

Your app.config should look like the following:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <compilation debug="true" />
  </system.web>
  <!-- When deploying the service library project, 
  the content of the config file must be added to the host's 
  app.config file. System.Configuration does not support config files for libraries. -->
  <system.serviceModel>
    <services>
      <service name="SoapToRESTfullDemo.ProductService" 
               behaviorConfiguration="SoapToRESTfullDemo.Service1Behavior">
        <host>
          <baseAddresses>
            <add baseAddress = "http://localhost:8888/products" />
          </baseAddresses>
        </host>
        <!-- Service Endpoints -->
        <!-- Unless fully qualified, address is relative to base address supplied above -->
        <endpoint address ="" binding="wsHttpBinding" 
              contract="SoapToRESTfullDemo.IProductService">
          <!-- 
              Upon deployment, the following identity element 
              should be removed or replaced to reflect the 
              identity under which the deployed service runs. 
              If removed, WCF will infer an appropriate identity 
              automatically.
          -->
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
        <!-- Metadata Endpoints -->
        <!-- The Metadata Exchange endpoint is used by the 
                service to describe itself to clients. -->
        <!-- This endpoint does not use a secure binding and 
                should be secured or removed before deployment -->
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="SoapToRESTfullDemo.Service1Behavior">
          <!-- To avoid disclosing metadata information, 
          set the value below to false and remove the metadata endpoint above before deployment -->
          <serviceMetadata httpGetEnabled="True"/>
          <!-- To receive exception details in faults for debugging purposes, 
          set the value below to true.  Set to false before deployment 
          to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="False" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
  <connectionStrings>
    <add name="NorthwindEntities" 
      connectionString="metadata=res://*/Northwind.csdl|res://*/Northwind.ssdl|res://
        */Northwind.msl;provider=System.Data.SqlClient;provider connection 
        string="data source=IDALTW76S51DS1;initial catalog=Northwind;
        integrated security=True;MultipleActiveResultSets=True;App=EntityFramework"" 
      providerName="System.Data.EntityClient" />
  </connectionStrings>
</configuration>

Running the REST Service

We basically expose the same functionality as we did using SOAP, but these functionalities are exposed through the standard HTTP uniform service contract. The URI will determine which functionality is being invoked. Let’s start by running the host. As seen in the following image, the host displays the base address, which is specified in the configuration file (http://localhost:8080/productservice).

Now, we can see the service invocation by typing in the full address in the Web browser; such ashttp://localhost:8080/productservice/products. This address will display the following result for the GetAllProducts(remember the UriTemplate is calling for “products”):

When calling GetProductByID, we will need to pass in the product ID as part of the query string; for instance,http://localhost:8080/product/productID. The following is the result, which returns only a product with an ID of 1:

Summary

This turns out to be extremely advantageous when building highly scalable web applications and services. Now we are able to represent resources using a concrete representation. A message format such as XML, RSS, JSON, etc. So, when we work with our resources when we request them or update them or create them, we’re going to be passing a representation of that resource at a particular point in time.

Although we can summarize things by saying that SOAP is usually a better fit within the enterprise, whereas REST is usually a better fit within public web facing service scenarios where you need a high degree of scalability and interoperability. The good news is WCF provides a programming model that accommodates each of these different styles and a variety of different message formats.

Ref: http://www.codeproject.com/Articles/590627/Converting-SOAP-based-WCF-Service-to-RESTful-Desig