将基于 SOAP 的 WCF 服务转成 RESTful 设计

jopen 11年前

介绍

当SOAP服务被大量的使用的时候, 一些开发者可能就会选用 RESTful services. 当开发者需要进行大范围的交互并且仅被限于使用基于HTTP协议传输的XML信息或JSON格式的信息时,通常会用到REST (Representative State Transfer) 。RESTful 服务与基于SOAP的服务是不同的,因为他们不能尝试的去达到中立性的传输形式。事实上, RESTful服务通常是绑定HTTP协议作为唯一的传输形式在整个系统中去使用的。 使用REST, 开发者能够模仿它们的服务作为一种资源,并且在URI指定的Form表单中给予这些资源唯一的标识。 在该文章中,我们将会把当前现有的基于SOAP协议的服务,转换成为一种更好的RESTful的设计实现。

背景

一种现有的 WCF SOAP 服务

在整个工业里面,SOAP在大量的工作中被广泛使用,它实现了一种全新的协议栈服务。这意味着如果我们想以自己的服务作为一种额外的特性或性能去实现它的话,那么就应该尽可能的以一种中立的传输方式去实现它。 我们将会在这个协议栈里面使用一个基于XML的信息层去实现它。 现在,在我们的脑海里SOAP已经形成了一张图纸。SOAP是一个特定XML词汇表,它用于去包装在我们的服务中需要去传输的信息。 以下是基于WCF服务的代码,在这之前,它需要转换为一个RESTful服务。 后端采用了实体框架,并使用了 Northwind示例数据库。然后它会跟随着一个图像,该图像是theGetAllProducts结果的一个样本, 它用于展示SOAP的请求和响应。

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      }  }

将基于 SOAP 的 WCF 服务转成 RESTful 设计

代码使用

转换

Rest的交互动作是通过一种标准的统一接口或服务规则来完成的。 在该例子中,它将定义的方法以GET,POST,PUT和DELETE的方式作为HTTP协议的传输。通过在统一接口上进行的标准化, 开发人员能够在每个操作的语义上构建出基础的架构,并使得该性能和可伸缩性能够做到尽可能的提高。 在安全方面, REST 仅仅使用了HTTP协议; 它只是利用SSL来满足它的安全需要。

我们将会进行三个操作:GetAllProducts, 这将会返回所有的product,GetProductByID 通过该操作我们将需要为product提供一个productID,然后,UpdateProduct 将会以Put的方式展示Web调用。

首先,我们要增加ServiceModel.Web程序集,它提供了对WebGet和WebInvoke接口的访问。下面是转换IProduct接口的每一步指令。

  1. 在Product接口里,我们定义 URI映射,指定映射到哪个URI;例如GetAllProducts的[WebGet(UriTemplate = "products")]
  2. 对于GetProductByID,我们需要传入基地址,后面跟着product,再跟着productID -[WebGet(UriTemplate = "product/{productID}")]
  3. WebInvoke使用同样的属性。update/submit 方法使用POST方法;例如,[WebInvoke(Method = "POST", UriTemplate = "product")]

完整的代码看上去像下面这样(正如你看到的,与基于SOAP方式的不同之处只是围绕着接口):

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      }  }

当接口修改完成后,我们可以修改配置文件 ( app.config) 以绑定服务。下面是修改 app.config 的步骤:

  1. 修改基础绑定到 WebHttpBinding - <endpoint address ="" binding="wsHttpBinding" contract="SoapToRESTfullDemo.IProductService">
  2. 添加新行为 - <behavior name="SoapToRESTfullDemo.Service1Behavior">
  3. 应用行为到服务器 - <service name="SoapToRESTfullDemo.ProductService" behaviorConfiguration="SoapToRESTfullDemo.Service1Behavior">

你的 app.config 应该是下面的样子

<?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>

运行REST服务

我们基本上暴露出了与使用SOAP时同样的功能,但这些功能是通过标准的HTTP统一服务合约暴露出来的。URI将决定哪一个功能被调用。我们由运行主机开始。像下面图片看到的,主机显示出基地址,这也就是在配置文件中指定的那个(http://localhost:8080/productservice)。

将基于 SOAP 的 WCF 服务转成 RESTful 设计

现在我们可以通过在Web浏览器中输入完整地址来观察服务的调用;比如http://localhost:8080/productservice/products。这个地址将显示GetAllProducts方法(记住UriTemplate会调用为"products")如下结果:

将基于 SOAP 的 WCF 服务转成 RESTful 设计

当调用GetProductByID时,我们需要传入product ID 作为查询字符串的一部分;例如, http://localhost:8080/product/productID。下面是结果,只是返回了一个ID为1的product:

将基于 SOAP 的 WCF 服务转成 RESTful 设计

总结

当创建高可伸缩性web应用与服务时,这会成为极其有利的条件。我们现在能够通过具体的表示方法表示资源。像XML, RSS, JSON,等等消息格式。因此,当我们处理我们的资源,当我们请求或者更新或者创建它们时,我们将在某个特定的时间点传送一个关于那个资源的表示。

虽然我们可以总结说,SOAP通常在企业环境下略微适用一些,而REST通常在面对公共web服务的场景稍稍适用,而且在那个环境中你需要高度的可伸缩性与可互通性。值得庆幸的是WCF提供了一个程序模型,以适应这些不同的模式以及各种各样不同的消息格式。