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:
- 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 - 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}")]
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();
[WebGet(UriTemplate = "product/{productID}")]
[OperationContract]
ProductEntity GetProductByID(string productID);
[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:
- Change binding from basic to WebHttpBinding -
<endpoint address ="" binding="wsHttpBinding" contract="SoapToRESTfullDemo.IProductService">
- Add a new behavior -
<behavior name="SoapToRESTfullDemo.Service1Behavior">
- Apply this behavior to the service -
<service name="SoapToRESTfullDemo.ProductService" behaviorConfiguration="SoapToRESTfullDemo.Service1Behavior">
Your app.config should look like the following:
="1.0"="utf-8"
<configuration>
<system.web>
<compilation debug="true" />
</system.web>
<system.serviceModel>
<services>
<service name="SoapToRESTfullDemo.ProductService"
behaviorConfiguration="SoapToRESTfullDemo.Service1Behavior">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8888/products" />
</baseAddresses>
</host>
<endpoint address ="" binding="wsHttpBinding"
contract="SoapToRESTfullDemo.IProductService">
<identity>
<dns value="localhost"/>
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="SoapToRESTfullDemo.Service1Behavior">
<serviceMetadata httpGetEnabled="True"/>
<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 as http://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.