将基于 SOAP 的 WCF 服务转换为 RESTful 设计
在本文中,我们将把一个现有的基于 SOAP 的服务转换为更 RESTful 的设计。
引言
当 SOAP 被认为过于复杂时,一些开发人员可能会选择 RESTful 服务。REST(Representational State Transfer,表述性状态转移)主要用于开发人员需要高度互操作性,并且希望将消息限制为通过 HTTP 传输的基本 XML 或 JSON 消息时。RESTful 服务与基于 SOAP 的服务在根本上是不同的,因为它们不追求传输的独立性。事实上,RESTful 服务通常将 HTTP 作为整个系统使用的唯一传输方式。使用 REST,开发人员可以将他们的服务建模为资源,并以 URI 的形式为这些资源分配唯一的标识符。在本文中,我们将把一个现有的基于 SOAP 的服务转换为更 RESTful 的设计。
背景
现有的 WCF 基于 SOAP 的服务
SOAP 是在行业内为实现全新的服务协议栈而进行的许多工作的基础上发展起来的。这意味着我们希望为服务实现的任何附加功能或能力都应该能够以传输无关的方式实现。而我们将通过使用一个基于 XML 的消息层在这个协议栈中实现这一点。现在,这就是 SOAP 登场的地方。SOAP 是一种特殊的 XML 词汇,用于打包我们需要传输到我们服务的消息。以下是 WCF 服务在转换为 RESTful 服务**之前**的代码。后端使用 Entity Framework 并利用 Northwind 数据库。然后是一个图像,显示了 GetAllProducts
的示例结果,其中包含 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
}
}
Using the Code
转换
REST 的交互通过标准的统一接口或服务契约来完成。在这种情况下,它将是 HTTP 协议定义的方法,特别是 GET、POST、PUT 和 DELETE。通过标准化统一接口,开发人员可以围绕每个操作的语义意义构建基础设施,并在可能的情况下进行性能和可伸缩性改进。对于安全性,REST 仅使用 HTTPS;它只是利用 SSL 来满足所有安全需求。
我们将从三个操作开始:GetAllProducts
,它返回所有产品;GetProductByID
,我们提供要查找的产品的 productID;最后,UpdateProduct
将演示 WebInvoke 操作,该操作使用 PUT 方法。
首先,我们需要添加 ServiceModel.Web 程序集,它使我们能够访问 WebGet
和 WebInvoke
方法。以下是将 IProduct
接口转换为 RESTful 接口的逐步说明:
- 在 Product 接口中,让我们定义 URI 映射,它指定要映射到的 URI;例如,对于
GetAllProducts
方法,使用[WebGet(UriTemplate = "products")]
。 - 对于
GetProductByID
,我们需要传入基址,然后是 product,然后是 productID -[WebGet(UriTemplate = "product/{productID}")]
。 WebInvoke
使用相同的属性。更新/提交方法使用 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 的步骤:
- 将绑定从 basic 更改为 WebHttpBinding -
<endpoint address ="" binding="wsHttpBinding" contract="SoapToRESTfullDemo.IProductService">
- 添加新行为 -
<behavior name="SoapToRESTfullDemo.Service1Behavior">
- 将此行为应用于服务 -
<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 = "https://: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 将决定调用哪个函数。让我们开始运行主机。如以下图像所示,主机显示了配置文件中指定的基址 (https://:8080/productservice)。
现在,我们可以通过在 Web 浏览器中键入完整地址来查看服务调用;例如 https://:8080/productservice/products。该地址将显示 GetAllProducts
的以下结果(请记住 UriTemplate
调用的是“products”)。
调用 GetProductByID
时,我们需要将 product ID 作为查询字符串的一部分传递;例如 https://:8080/product/productID。以下是结果,它只返回 ID 为 1 的产品。
摘要
这对于构建高度可扩展的 Web 应用程序和服务非常有益。现在我们能够使用具体的表示形式来表示资源。如 XML、RSS、JSON 等消息格式。因此,当我们处理资源时,无论是请求、更新还是创建它们,我们都将在特定时间点传递该资源的表示。
尽管我们可以总结说,SOAP 通常更适合企业环境,而 REST 通常更适合需要高度可伸缩性和互操作性的面向公众的 Web 服务场景。好消息是 WCF 提供了一个编程模型,该模型可以适应这些不同的样式和各种不同的消息格式。