將基于 SOAP 的 WCF 服務轉成 RESTful 設計
介紹
當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 }
}</pre>
![]()
代碼使用
轉換
Rest的交互動作是通過一種標準的統一接口或服務規則來完成的。 在該例子中,它將定義的方法以GET,POST,PUT和DELETE的方式作為HTTP協議的傳輸。通過在統一接口上進行的標準化, 開發人員能夠在每個操作的語義上構建出基礎的架構,并使得該性能和可伸縮性能夠做到盡可能的提高。 在安全方面, REST 僅僅使用了HTTP協議; 它只是利用SSL來滿足它的安全需要。
我們將會進行三個操作:GetAllProducts, 這將會返回所有的product,GetProductByID 通過該操作我們將需要為product提供一個productID,然后,UpdateProduct 將會以Put的方式展示Web調用。
首先,我們要增加ServiceModel.Web程序集,它提供了對WebGet和WebInvoke接口的訪問。下面是轉換IProduct接口的每一步指令。
- 在Product接口里,我們定義 URI映射,指定映射到哪個URI;例如GetAllProducts的[WebGet(UriTemplate = "products")]
- 對于GetProductByID,我們需要傳入基地址,后面跟著product,再跟著productID -[WebGet(UriTemplate = "product/{productID}")]
- WebInvoke使用同樣的屬性。update/submit 方法使用POST方法;例如,[WebInvoke(Method = "POST", UriTemplate = "product")]
</ol>完整的代碼看上去像下面這樣(正如你看到的,與基于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 }
}</pre>
當接口修改完成后,我們可以修改配置文件 ( app.config) 以綁定服務。下面是修改 app.config 的步驟:
- 修改基礎綁定到 WebHttpBinding - <endpoint address ="" binding="wsHttpBinding" contract="SoapToRESTfullDemo.IProductService">
- 添加新行為 - <behavior name="SoapToRESTfullDemo.Service1Behavior">
- 應用行為到服務器 - <service name="SoapToRESTfullDemo.ProductService" behaviorConfiguration="SoapToRESTfullDemo.Service1Behavior">
</ol>你的 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)。
![]()
現在我們可以通過在Web瀏覽器中輸入完整地址來觀察服務的調用;比如http://localhost:8080/productservice/products。這個地址將顯示GetAllProducts方法(記住UriTemplate會調用為"products")如下結果:
![]()
當調用GetProductByID時,我們需要傳入product ID 作為查詢字符串的一部分;例如, http://localhost:8080/product/productID。下面是結果,只是返回了一個ID為1的product:
![]()
總結
當創建高可伸縮性web應用與服務時,這會成為極其有利的條件。我們現在能夠通過具體的表示方法表示資源。像XML, RSS, JSON,等等消息格式。因此,當我們處理我們的資源,當我們請求或者更新或者創建它們時,我們將在某個特定的時間點傳送一個關于那個資源的表示。
雖然我們可以總結說,SOAP通常在企業環境下略微適用一些,而REST通常在面對公共web服務的場景稍稍適用,而且在那個環境中你需要高度的可伸縮性與可互通性。值得慶幸的是WCF提供了一個程序模型,以適應這些不同的模式以及各種各樣不同的消息格式。