通常企业使用两种架构实现Web服务:基于SOAP的服务和基于REST模型的服务。这两个架构依赖HTTP协议和因特网实现的寻址体系,但采取不同的方式使用它们。到目前为止,本书所有的练习都集中在SOAP模型上,这种架构使得Web服务的设计者关注服务的业务过程,并把这些业务过程对外公开为服务的操作。相比之下,REST模型则关注企业对外公布的数据,并实现一个允许客户端程序使用自身的逻辑访问并操作这些数据的体系。REST模型变得越来越普遍,你可以使用WCF提供的特性、方法、和类型快捷地构建和访问REST Web服务。此外,实体框架为WCF提供了数据服务模板,你可以使用这些模板把来自实体框架模型的数据和实体作为REST资源对客户端程序公开。
本章的学习目的是介绍REST Web服务,并展示如何使用WCF创建和访问REST Web服务。
理解REST模型
REST模型最早在2000年由Roy Fielding在他的博士论文中提到,"结构化样式和基于网络软件的架构设计"。正如论文的名字那样,REST是一个架构风格而不是一个预先定义的构建Web服务的方式,你可以使用任何合适的技术实现REST。REST的关键点在于它描述了一个无状态的,遵循与WWW相似的架构。举例来说,AdventureWorks提供客户和销售信息数据的访问,把每个客户或订单的详细信息作为单个的资源。从AdventureWorks网站获取所有客户列表,那么应有如下URL的网站应用程序:http://www.adventure-works.com/sales/customers。数据可以按照多种格式返回,但是最常见的模式包括XML和JSON。如果AdventureWorks选择XML,那么通过上述URL查询的返回结果可能如下所示:
遵循REST模型的Web服务一般允许Web应用程序通过额外的路径参数进一步提取数据。比如,查询单个客户的详细信息。如果客户的编号为99,那么AdventureWorks允许Web应用程序指定如下的URL地址:http://www.adventure-works.com/sales/customers/99。 此外,允许Web应用程序查找某个指定客户的所有订单也是非常有用的,那么对于的URL应该为:http://www.adventure-works.com/sales/customers/99/orders。
设计基于REST方案的关键是要理解如何划分业务模型为资源,以及如何将这些资源关联到一起。在某些情况下,比如客户和订单,这种关系相对简单明了,但是在其他的一些方案中要划分和关联资源无疑是一项艰巨的任务。
REST模型依赖于应用程序,这些程序发送合适的HTTP动作,这些动作构成请求的一部分以访问服务的数据。举例来说,上面所述的请求发送了一个HTTP GET请求至Web服务。HTTP还支持其他动作,比如POST,PUT和DELETE,这些动作对应REST服务的创建,修改和移除资源。使用REST模型,你可以利用这些动作创建插入、更新和删除数据。
与SOAP相反,REST模型发送和接收的消息更简洁。这主要是因为REST WS-*的描述不需要提供路由,策略,或安全等功能;你仅仅需要依赖Web服务器实现的,用以保护REST Web服务的基础传输结构。你还应该记住REST模型是无状态的;在客户端与服务的交互对话之间不存在会话或者事务的概念。因而采取简单实现方式的REST Web服务意味着在传输和接受消息方面通常比SOAP Web服务更有效率。
实现一个查询数据的REST Web服务
使用WCF实现一个REST Web服务是一个简单的过程,而且WCF在System.ServiceModel.Model组件中提供了许多类型供你使用。该过程最重要的部分是设计服务对外提供资源读取的数据体系。在大多数情况下,资源根据自然状态分组到各个集合中,并且与其他资源或者集合之间相互关联。本章的练习将使用下表定义的数据体系。
URI |
描述 |
Customers |
AdventureWorks数据库中所有的客户 |
Customers/{customerID} |
某一个指定的客户 |
Orders |
AdventureWorks数据库中所有的订单 |
Orders/{orderID} |
某一个指定的订单 |
Orders/{orderID}/Customer |
某一个订单对应的客户 |
Customers/{customerID}/Orders |
某一个客户的所有订单 |
根据数据库中数据的数量,Customers和Orders URIs可能返回大量数据项。因次,根据用户额外指定的查询参数以限制返回的数据项是非常有意义的。在本节的练习中,你将实现两个可选的查询参数,它们分别为skip和top,用户可以按照下面的方式使用这两个参数:
Orders?top=10
Orders?skip=500
Orders?skip=9&top=20
Top参数的目的是仅仅获取前n条数据记录,n为参数top的数值。上面的第一个例子从数据库中获取前10个订单。Skip参数的目的是忽略前n条数据记录然后从第n+1条记录开始获取数据记录。上面的第二个例子是从数据库中获取第501条开始的所有订单。第三个例子就是从第10条开始提取数据并提取20条,也就是提取从10到30的数据记录。使用这种方式合并参数允许客户端实现分页机制--以一种数据块大小可管理的方式获取数据。
与SOAP Web服务一样,实现REST Web服务的第一个任务是定义服务合约。合约指定对外公开的操作,并在操作与URIs之间建立关联以区分不同的资源。然后Web客户端程序就可以通过查询URIs调用这些操作。
练习:定义ProductsSales REST Web服务合约
1. 启动Visual Studio,并根据下表的内容创建一个新的解决方案
URI |
描述 |
模板 |
空白解决方案 |
名字 |
ProductsSales |
位置 |
Solutions\WCF\Step.by.Step\Chapter15 |
2. 添加一个新的类库项目到ProductsSales解决方案。将其命名为ProductsSalesService,该项目的保存位置为Solutions\WCF\Step.by.Step\Chapter15\ProductsSales
3. 在解决方案浏览器窗口中,添加ProductsSalesModel.edmx文件和App.config文件至ProductsSalesService项目。这两个文件可以从原书作者的源代码中的Chapter15文件夹中找到。
ProductsSalesModel.edmx文件是一个实体框架模型。它定义了两个实体,分别为Contact和SalesOrderHeader。在AdventureWorks数据库中,Contacts表包含了所有联系人和客户的详细信息,SalesOrderHeader表包含了客户开出的订单信息。App.config文件包含了供实体模型用以连接至AdventureWorks数据库的连接字符串。
4. 重新生成解决方案。该行为将从实体模型中生成Contact类和SalesOrderHeader类对应的代码。
5. 引用System.ServiceModel组件和System.ServiceModel.Web组件到ProductsSalesService项目。
6. 在解决方案浏览器窗口,更改Class1.cs文件的名字为IProductsSales.cs。并允许Visual Studio更新所有对与Class1.cs的引用。
7. 在文本编辑模式下打开IProductsSales.cs文件,添加下面的using声明到文件的顶部。
8. 更改IProductsSales类为一个公共的接口,为添加ServiceContract特性。
9. 在IProductsSales接口中,添加GetAllOrders方法,并为该方法添加特性:
该操作将返回SalesOrderHeader对象集合,其包含了每个订单的数据。OperationContract特性标明该方法为一个Web服务操作。WebGet特性标明这是一个REST操作,该操作响应HTTP GET请求并同过UriTemplate属性指定了客户端程序用以调用该操作的URI。该URI包含了可选的skip查询参数和top查询参数。在花括号中的ship和top可以被替换成客户端程序运行时所提供的值。GetAllOrders方法的定义中所包含的参数与上述两个参数有相同的名字;WCF运行时将把客户端程序传递的值对应地填充到GetAllOrders方法的参数上。请注意,虽然服务操作参数的顺序并不重要,但是这些参数的名字必须与WebGet特性类中UriTemplate属性值中花括号内的名字一样。
使用Description特性,你可以指定一些简单的描述性文字,这些文字会显示在服务的帮助页面中。
10. 添加GetOrder方法到IProductsSales接口中:
当客户端程序通过制定的URI格式Orders/OrderId访问服务时该操作运行,它将获取与订单对应的SalesOrderHeader对象。与上一步一样,注意该方法接收一个与UriTemplate花括号中展位符相匹配的参数。另一个值得关注地方是该参数的类型是字符串类型,尽管AdventureWorks数据中SalesOrderHeader表中对应列的数据类型为整数类型。这是因为URI中的导航元素必须为字符串类型。该限制并不适用于查询参数(GetAllOrders方法中skip和top参数就是整数类型)
11. 添加GetCustomerForOrder方法到IPorductsSales接口中:
当客户端程序通过制定的URI格式Orders/{OrderId}/Customer访问服务时该操作运行。
12. 添加GetAllCustomers,GetCustomer,和GetOrdersForCustomer方法到IProductsSales接口:
上面三个方法定义的操作允许客户端程序分别获取所有的客户,某个特性客户的详细信息,以及某个特定客户的所有订单。
13. 重新生成解决方案。
练习:实现ProductsSales REST Web服务
1. 添加一个新的类文件到项目ProductsSalesService项目中,并将其命名为ProductsSales.cs。
2. 在文本编辑模式下打开ProductsSales.cs文件。然后在该文件的头部添加下面的using声明
3. 修改ProductsSales类的定义,使其实现IProductsSales接口
4. 添加下面的GetAllOrders方法到ProductsSales类
该方法从AdventureWorks数据库中获取订单列表,最后返回SalesOrderHeader对象集合。Web应用程序通过寄宿该服务的URI来访问订单时调用该方法。Web应用程序可能还可以可选性地通过skip和top这两个参数来查询订单。这两个参数的默认值为0。由于数据据可能包含成百上千的数据订单记录。因此服务仅仅从数据库中提取前100条订单数据。当然用户可以显示地指定所需订单的确切数量。如果应用程序需要数据库中所有订单,那么它可以不停的调用GetAllOrders操作,并在每次调用GetAllOrders时为skip参数和top参数指定适当的值。
该方法并没有验证参数和检查错误;比如,它并没有验证skip和top参数是否为负数。如果异常发生,该方法抛出WebFaultException异常。该异常类型是一个特定版本FaultException类,唯一的区别是该异常生成一个HTTP错误而不是SOA错误。 WebFaultException异常构造函数的参数指定HTTP错误包含的HTTP状态代码。 Http状态代码是位于System.Net命名空间下的一个枚举类型,它定义了一个代码列表,你可以将其中的代码返回给一个Web应用程序。HttpStatusCode.BadRequest对应的代码为400错误消息,该针对所有异常都返回400错误消息,不包含详细的错误信息。由于安全原因,你应当避免在错误发生时返回太多的信息(尽管常见的最佳实践是记录错误的详细信息到本地服务器上,一般都是记录在系统事件日志的程序日志中)。
5. 添加GetOrder方法到ProductsSales类
该方法根据传入参数(订单编号的值)找到订单并返回SalesOrderHeader对象。注意Web程序提供的订单编号是字符类型,因此在方法中我们需要将其转化为整数类型。如果没有找到订单,则返回一个null值。和前一个方法一样,如果发生异常,则返回错误请求错误。
6. 添加GetCustomerForOrder方法到ProductsSales类
上述方法获取开出指定订单的客户。为了完成该操作,LINQ查询通过ContractID列和CustomerID列连接AdventureWorks数据库中的Contact表与SalesOrderHeader表。查询完成后,返回一个Contact对象。
7. 添加GetAllCustomer,GetCustomer,和GetOrdersForCustomer方法到ProductsSales类
上面三个方法与前面的步骤类似,不同的仅仅是聚焦于客户而不是订单罢了。
8. 重新生成解决方案
与SOAP Web服务一样,你可以使用IIS,或者创建自己的程序寄宿REST Web服务。 WCF在System.ServiceModel.Web命名空间下提供了WebServiceHost类,该类是一个特定版本的ServiceHost类,其为REST Web服务提供了寄宿环境。WebServiceHost类为其寄宿的服务增加了一个名为WebHttpBehavior的端点行为。该行为使服务能使用HTTP请求而不是SOAP请求接受和发送消息。你将在下面的练习中使用该类。
练习:寄宿ProductsSales REST Web服务
1. 添加一个新的控制台程序项目到ProductsSales解决方案,并将其重命名为ProductsSalesHost,该项目的保存位置为Solutions\WCF\Step.by.Step\Chapter15\ProductsSales文件夹中。
2. 打开ProductsSalesHost项目的属性页。然后点击程序标签,设置Traget Framework属性为.NET Framework 4,并允许Visual Studio关闭和重新打开该项目。
3. 引用ProductsSalesService项目到ProductsSalesHost。然后引用System.ServiceModel组件和System.ServiceModel.Web。
4. 在文本编辑模式下打开Program.cs文件,然后添加下面的using声明:
5. 在Programm类中,添加下面的声明:
上述代码基于ProductsSalesService类创建一个WebServiceHost对象,然后启动该服务并侦听请求。上述代码与之前章节中寄宿SOAP服务的代码是相似的。
6. 删除ProductsSalesHost的app.config文件,并从原书所附代码中的Chapter15文件夹下拷贝App.config到ProductsSalesHost项目中。
7. 使用WCF服务配置管理器打开ProductsSalesHost下的App.config文件。
8. 在配置面板,在服务文件夹上点击右键,然后点击创建新的服务。然后在右边面板中,在名字字段处,输入ProductsSalesService.ProductsSales。
9. 在配置面板,展开ProductsSalesService.ProductsSales服务文件夹,然后在文件夹下的端点处击右键选择创建新的服务端点,并按照下表的值设置新创建服务端点的对应属性。
属性 |
值 |
地址 |
http://localhost:8000/Sales |
绑定 |
webHttpBinding |
合约 |
ProductsSalesService.IProductsSales |
IProductsService服务中的操作上的WebGet特性类中的uriTemplate属性指定的URIs将应用到服务的相对地址上。比如,GetCustomerForOrder操作UriTemplate属性的值为Orders/{orderID}/Customer。那么客户端程序就可以通过 http://localhost:8000/Sales/Orders/54545/Customer 来调用GetCustomerForOrder操作,其中54545是订单编号。
webHttpBinding绑定配置为REST Web服务配置了一个端点,该端点对外公接收HTTP请求而不是SOAP消息。
10. 保存配置文件并推出WCF服务配置管理器
11. 重新生成解决方案。
当你从Web引用程序中提交一个HTTP GET请求后,ProductsSales服务将响应该请求,你可以使用Web浏览器比如IE来向服务发送请求,在下面的练习中我们将使用IE想ProductsSales服务发送请求。请注意WebServiceHost类禁用了对外公布WSDL元数据。但是,当REST Web服务启动了帮助后,你仍然可以查询该REST服务。因此,在下面的练习中,你将配置ProductsSalesService服务提供帮助信息,列出服务所支持的URLs,当你使用每个URI查询时构造返回数据。
练习:使用Web浏览器测试ProductsSales REST Web服务
1. 在解决方案浏览器窗口中,设置ProductsSalesHost项目为解决方案的启动项目,然后在非调试模式下启动解决方案。确认ProductsSalesHost程序的控制台窗口显示消息"Service Running"。
2. 启动IE,然后输入地址栏http://localhost:8000/sales/orders,然后按ENTER键。浏览器访问该URI将调用GetOrders操作, 该操作返回一个SalesOrderHeader对象集合,该集合被序列化为XML。 IE显示序列化后的XML数据,如下图所示:
3. 指定另外一个URL:http://localhost:8000/Slaes/Orders?skip=100&top=5,然后检查返回结果。这次你会发现第一个销售订单编号为43759。
4. 然后你还可以分别测试其他的一写URL,检查各种正确和错误的结果
URL |
结果 |
http://localhost:8000/Sales/Orders?top=-10 |
返回错误也页面 |
http://localhost:8000/Sales/Orders/43700 |
返回定边编号为43700的订单详细内容 |
http://localhost:8000/Sales/Orders/30000 |
返回空白页面,因为订单编码30000不能找到 |
http://localhost:8000/Sales/Orders/43700/Customer |
范围订单号码为43700的客户信息 |
http://localhost:8000/Sales/Customers |
返回前100个客户信息 |
http://localhost:8000/Sales/Customers/100 |
客户编号为100的客户信息 |
http://localhost:8000/Sales/Customers/100/Orders |
客户编号为100的客户的所有订单 |
5. 返回到ProductsSales服务的控制台程序窗口,按ENTER键停止服务,然后关闭控制台窗口,但是保持IE处于运行状态。
6. 在Visual Studio中,使用WCF服务配置编辑器打开App.config
7. 在配置面板,展开高级文件夹,在端点行为上点击右键,然后点击创建一个新的端点行为。在右边面板中,清楚名字的值,然后在右下角,点击添加按钮以添加一个webHttp行为扩展元素到当前端点的行为中。
8. 在配置面板,点击未命名端点下的webHttp节点。在WebHttp面板,设置helpEnabled属性为true。该属性允许REST Web服务公布一个帮助页面,该页面描述服务的所有操作,服务响应消息的XML结构,以及响应消息的JSON结构。
9. 保存配置文件,然后关闭WCF服务配置编辑器。
10. 在Visual Studio 中,在非调试模式下运行解决方案。
11. 返回到IE中,然后在地址栏输入http://localhost:8000/Sales/help。IE将转向ProductsService服务的帮助页面。其内容如下图所示:
12. 点击Customers/{customerID} URI的GET链接。另外一个描述GetCustomer操作响应消息的页面将出现:
13. 检查其他操作对应的响应消息的格式,然后关闭IE。
14. 返回到ProductsSales服务的控制台程序窗口,按ENTER键停止服务,然后关闭控制台窗口。
使用Web程序通过URI发送请求与使用浏览器发送请求是一样地,此外你还可以过程性的客户端应用调用REST Web服务的操作。构建这样一个客户端程序与构建程序访问SOAP Web服务是相似的;你可以创建一个代理类然后使用常规方式调用服务公开的方法。不幸的是,Visual Studio当前并没有为REST Web服务提供生产代理类的功能;但是,通过扩展System.ServiceModel.ClientBase类手动实现一个代理类(请参考第十一章)并不困难。下面的联系我们将使用这种方式创建客户端程序访问REST Web服务。
练习:创建过程化客户端程序访问ProductsSales REST Web服务
1. 在Visual Studio中,添加一个新的控制台程序,并将其命名为ProductsSalesClient,该项目的保存位置为Solutions\WCF\Step.by.Step\Chapter15\ProductsSales文件夹中。
2. 打开ProductsSalesClient项目的属性页,设置TargetFramework属性为.NET Framework 4。并允许Visual Studio关闭然后重新打开该项目。
3. 引用项目ProductsSalesService到ProductsSalesClient;然后引用System.Data.Entity,System.ServiceModel组件到ProductsSalesClient项目。
4. 添加一个新的类文件到ProductsSalesClient项目,并命名该文件为ProductsSalesProxy.cs。
5. 然文本编辑模式下打开ProductsSalesProxy.cs文件。然后添加下面的using声明到该文件的头部:
6. 修改ProductsSalesProxy类的定义以使其派生ClientBase类,并实现IProductsSales接口:
7. 实现IProductsSales接口的GetAllOrders方法:
正如第十一章的描述,该方法简单地路由调用经过与服务相连接的通道,然后调用服务中相同名字的操作。该方法指定了skip参数和top参数为可以选参数,它们的默认值均为0。
8. 添加GetOrder,GetCustomerForOrder,GetAllCustomers,GetCustomer和GetOrdersForCustomer方法到ProductsSalesProxy类。这些方法与GetAllOrders采用同样的模式实现;他们都通过与服务连接的通道调用服务中对应的方法。
9. 在文本编辑模式下打开ProductsSalesClient项目下的Program.cs文件,然后添加下面的using声明到该文件的头部:
10. 添加下面的语句到Main方法中:
上面的代码创建ProductsSalesProxy类的一个实例,然后功过带代理调用每个可能的方法以执行下面的测试:
- 使用GetAllOrders获取所有订单,并为skip参数和top参数指定值。
- 通过GetOrder方法获取订单编号为43687的详细信息
- 通过GetCustomerForOrder方法获取订单编号为43867的客户信息
- 通过GetAllCustomers方法获取订单编码在75和90之间的客户信息
- 通过GetCustomer方法获取客户编号为99的客户信息
- 通过GetOrdersForCustomer方法获取客户编号为99的所有订单信息
11. 使用WCF配置编辑器打开ProductsSalesClient项目下的App.config文件。
12. 在配置面板,展开客户端文件夹,然后在端点上点击右键,然后点击创建新的客户端端点。
13. 在客户端端点面板,按照下表的值设置新创建端点的对应属性。
属性 |
值 |
地址 |
http://localhost:8000/Sales |
绑定 |
webHttpBinding |
合约 |
ProductsSalesService.IProductsSales |
14. 在配置面板,展开高级文件夹,然后在端点行为上电价右键,然后点击创建新的端点行为配置。在右边面板,清楚名字的值。然后在面板的右下方,点击添加按钮,然后添加一个webHttp行为扩展元素到端点行为中。
注意:与使用WebServiceHost类寄宿服务不同,访问REST服务的客户端程序的端点不会自动地被配置为发送HTTP请求,因为你必须总是添加WebHttpBehavior行为到客户端端点中。
15. 保存配置文件,并退出WCF服务配置编辑器。
16. 在Visual Studio中,只是ProductsSalesClient项目和ProdudtsSalesHost项目均为解决方案的启动项目。然后在非调试模式下启动解决方案。
17. 在ProductsSalesClient控制台窗口中,按下ENTER键。各个测试都将成功地运行,并逐步的产生如下图所示的结果:
18. 在ProductsSalesClient控制台窗口中按ENTER键以关闭客户程序;然后在ProductsSalesHost控制台中按ENTER键以关闭寄宿程序。