使用WCF数据服务
到目前为止,本章中为你展示的例子都属于比较常见的情形。它们演示了如何使用WCF构建REST Web服务,这些服务使用WebGet和WebInvoke特性类手动定义一个对外公布数据的体系。如果你构建REST Web服务时采用ADO.NET实体框架读取数据,那么你可以使用WCF数据服务自动地完成大部分任务。使用由Visual Studio 2010提供的WCF Data服务模板和组件,你可以创建由WCF数据服务提供的抽象层,然后使用该抽象层构建REST Web服务。
你可以添加一个WCF数据服务到一个Web应用程序中以实现WCF数据服务。首先你需要定义一个实体框架,该实体框架描述对外公布的数据,然后添加WCF数据服务到该Web应用程序中。WCF数据服务模板基于System.Data.Service.DataService类型生成相关的数据服务类:
该类(DataService)的类型参数是供实体模型使用的ObjectContext类,你应当使用这个类型来替代上面代码中的注释。当服务开始运行时,InitializeService方法自动执行。你可以添加服务公布的实体模型中实体和指定客户端程序对于这些实体数据的读取权限的代码到InitializeService方法中。比如,你可以指明一组实体中的数据是只读的,而在另外一组实体中的数据是可写的。下面的一组练习介绍了如何使用WCF数据服务模板创建和消耗REST Web服务。
练习:构建一个公布销售信息的WCF数据服务
1. 启动Visual Studio,使用ASP.NE空网站模板创建一个新的Web站点。该网站保存在本地文件系统的Solutions\WCF\Step.by.Step\Chapter15文件夹下。
2. 在解决方案浏览器窗口中,点击C:\...\SalesData工程。在属性窗口中,设置自动端口属性为false,然后设置端口的号码为48000。
3. 在解决方案浏览器窗口,在C:\...\SalesData工程上点击右键,然后点击添加新的项目。在添加新项目对话框中,选择ADO.NET实体数据模板。在名字字段处,输入SalesDataModel.edmx,然后点击添加按钮。当Visual Studio出现提示对话框时,允许Visual Studio添加该数据模型到工程的App_Code文件夹下。
4. 在实体数据模型向导中,在选择模型内容页,点击从数据库生成,然后点击下一步。
5. 在选择数据连接页,点击创建新连接。在连接属性对话框,在服务名字段处,输入.\SQLExpress。在选择或者输入数据库名字字段处,输入AdventureWorks,然后点击确认按钮。
6. 在选择数据连接页,确认保存实体连接设置到Web.config前的复选框被选中,如有必要更改连接字符串的名字为AdventureWorksEntities。然后点击下一步。
7. 在选择数据对象页,选择Contact(Person), SalesOrderDetail(Sales), 和SalesOrderHeader(Sales)表。对于其他选项请确认与下表中的值相同;然后点击完成按钮。
选项 |
值 |
生成对象的名字使用单数或复数 |
选中 |
在模型中包括外键列 |
选中 |
模型命名空间 |
AdventureWorksModel< |
最后的实体模型如下图所示:
当一个客户下订单时,其订单可能包含多个子项。SalesOrderHeader表包含了订单的信息(比如订单日期,客户的ID,等等),而SalesOrderDetail表则为订单的每个子项创建一行记录(比如产品ID,订购数量,等等)。
8. 生成解决方案。
9. 在解决方案浏览器窗口,添加另外一个新的项目到C:\...\SalesData工程中。在添加新项目对话框中,选择WCF数据服务模板。在名字字段处,输入SalesDataService.svc,然后点击添加按钮。
Visual Studio模板将生成一个新的名为SalesDataService.cs文件,该文件包含了一个名为SalesDataService的类,如前所述,该类继承了DataService类。
10. 在文本编辑模式下,打开SalesDataService.cs文件中,然后添加下面的using声明到该文件的头部
11. 在SalesDataService类的定义中删除注释。然后修改其为如下面的代码所示定义:
AdventureWorksEntites类是由ADO.NET实体模型向导自动生成的ObjectContext类型,该类型用以访问由该实体模型呈现的数据。
出于安全的原因,WCF数据服务模板并没有自动地对外公布任何资源,比如实体模型实现的实体集合。你必须在InitailizeService方法中制定允许或者禁止访问资源的策略。该方法接受一个DataServiceConfiguration对象为其参数,在该对象中你可以定义资源的访问策略。
12. 在InitializeService方法中,删除注释,然后添加下面的代码:
DataServiceConfiguration类的SetEntitySetAccessRule方法用以指定客户端程序对实体模型定义的每个实体的访问级别。在本练习中,WCF数据服务仅仅允许对数据进行只读操作。
SetEntitySetAccessRule方法接收两个参数
- 实体集名称。在实体模型中,这参数与一个实体集合的名字一样,但是参数使用复数的形式。该参数的值可以包含*通配符以标明所有实体集合,尽管这么做并不值得推荐。
- 对实体集授予的访问权限。该参数的值来自于System.Data.Service.EntitySetRights枚举类型。该枚举定义了各种读和写的访问权限。你可以使用或运算符合并实体集合权限。下表总结了EntitySetRights枚举的值:
选项 |
值 |
None |
拒绝访问数据。所有实体的默认设置。 |
ReadSingle |
授权读取实体集中单个项目
|
ReadMultiple |
授权读取数据集 |
WriteAppend |
授权在数据集中穿件一个新的数据项 |
WriteReplace |
授权替换数据 |
WriteDelete |
授权从数据集中删除数据项 |
WriteMerge |
授权合并数据 |
AllRead |
ReadSingle或ReadMultiple的缩写 |
AllWrite |
WriteAppend或WriteReplace或WriteDelete或WriteMerge的缩写 |
All< |
所有读和所有写操作的缩写 |
13. 请注意InitializeService方法的最后一行。每个实体集(Contacts,SalesOrderHeaders,SalesOrderDetails)可能包含数千行数据,客户端程序可能使用一个查询获取所有的数据。为了预防没有限制的查询耗费网络带宽,SetEntityPageSize方法限制查询返回的项目的数量。在本练习中该数值为25。SetEntityPageSize方法的第一个参数用于指定实体的名字,同样地该参数可以使用通配符,以表示适用于所有的实体。然而,与SetEntitySetAccessRule方法不同的是,SetEntityPageSize中使用通配符*是值得推荐的一种做法。
14. 生成解决方案。
这就是使用WCF数据服务模板构建REST Web服务所需做的事情。当服务运行时,DataService类基于实体模型中实体的名字和实体之间的关系构成的体系自动地发布数据。你可以指定匹配实体模型架构的URI。WCF数据服务提供一系列的操作,你可以使用这些操作有选择地从单个列中获取数据,排序数据,或者基于数据做聚合计算。在接下来的练习中你将检查这些操作。
练习:测试WCF数据服务SalesData
1. 在解决方案浏览器窗口,在文本模式下打开SalesDataService.svc文件,该文件包含下面的代码:
WCF运行时使用该文件中的信息确定如何启动SalesDataService服务。Factory特性标明DataServiceHostFactory类型位于System.Data.Services命名空间下。该类型由WCF数据服务提供,并且它的目的是根据Service特性指定的类型创建一个WCF数据服务实例,并调用该类型(SalesDataService)的InitializeService方法设置安全策略,然后启动服务运行。
2. 在SalesDataService.svc文件上点击右键,然后点击在浏览器中查看。服务将开始运行,IE启动并显示如下的内容:
3. 在IE的地址栏中输入http://localhost:48000/SalesData/SalesDataService.svc/Contacts。 IE将显示AdventureWorks数据库中前25个联系人。数据以Atom信息来源格式显示。其内容如下图所示:
注意:由于IE配置的不同,可能你需要关闭feed-off视图以按照上图显示数据。关闭的方法是:IE—工具—IE选项—内容标签—信息来源(Feeds)--设置—取消启动信息来源阅读视图。然后点击确认按钮关闭信息来源设置对话框;然后点击确认关闭IE选项对话框;然后重启IE,并输入地址http://localhost:48000/SalesData/SalesDataService.svc/Contacts,你将得到上图所示的结果。
4. 在IE地址栏输入http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderDetials。SalesOrderDetails表中的前25条记录将显示在IE中。
如果你需要访问前25条数据之外的数据,WCF数据服务允许在查询中使用$skip和$top选项以块方式获取数据。这些查询选项与第一节中的skip和top参数类似。
5. 在IE地址栏输入http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderDetials?$skip=50。
你将从AdventureWorks数据的SalesOrderDetials表中51条记录开始获取25条数据并显示在IE中。请注意显示的数据以SalesOrderID列升序排列,该列在数据库中为SalesOrderDetails表的主键。
6. 在IE地址栏输入http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderDetials?$orderby=UnitPrice。那么现在数据按照UnitPrice升序排列。你还可以切换为按照UnitPrice降序排列:http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderDetials?$orderby=UnitPrice desc
7. WCF Data服务提供了从实体中提取数据的操作。比如:http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderHeaders?$select=SalesOrderID,OrderDate,CustomerID,TotalDue。
该URI形成一个映射以限制返回的数据仅仅为SalesOrderID, OrderDate, CustomerID和TotalDue列。你还可以指定前缀以限制返回的数据项是根据通过$filter选项执行的值执行查询的。下面的URI显示客户编号为99的客户的所有订单的SalesOrderID和TotalDue:http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderHeaders?$select=SalesOrderID,TotalDue&$filter=CustomerID eq 99
8. WCF数据服务还非常方便的遍历实体与相关数据值之间的关系。在IE地址栏输入下面的URI:http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderHeaders(43682)。
该URI获取根据AdventureWorks数据库中主键列SalesOrderID的值,单个SalesOrderDetial项目。如果你想查找下此订单联系人的详细信息,那么你只需要在该URI后面添加"/Contact":http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderHeaders(43682)/Contact。
为了查找该订单的详细信息,你只需要在该URI的后面添加/SalesOrderDetails:
http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderHeaders(43682)/SalesOrderDetails。
9. 在IE地址栏输入URI: http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderHeaders?expand=Contact
该查询获取SalesOrderHeader的前25行,但是$expand选项会使WCF Data服务同时获取与每个订单关联的联系人的信息。
10. 关闭IE并返回到Visual Studio。
你已经构建了一个WCF 数据服务,并且学习了如何通过一个Web浏览器访问该服务。该服务使用ASP.NET开发Web服务器寄宿WCF数据服务,当然现在你应该已经可以非常容易地将其部署到IIS上。此外,你还可以与本章第一节一样,使用WebServiceHost构建一个自定义的宿主程序寄宿此服务。
使用客户端程序消耗WCF数据服务
SalesData服务非常简单,使用该服务,用户可以执行许多复杂的查询。尽管AtomPub协议已经被广泛接受,但是使用该协议返回的数据格式却不易理解和转换;你不能期望终端用户能理解这些数据格式。幸运地是,你可以通过构建客户端程序消耗WCF数据服务,这样服务端的数据以一种更容易理解的格式呈现给终端用户。为了实现该目标,你必须生成一个WCF数据服务客户端库。该客户端将做为WCF数据服务的代理,以读取WCF数据服务公布的数据。
你可以使用两种方式生成客户端库:在命令行中使用DataSvcUtil实用工具,或者在Visual Studio中通过添加服务引用向导。如果使用DataSvcUtil工具,在WCF数据服务在运行时,打开Visual Studio命令行工具然后输入下面命令(红色划线部分):
该命令将创建一个名为SalesClient.cs的代码文件,该文件包含了客户端程序用于向WCF数据服务发送请求的方法。客户端库通过与WCF数据服务下的实体模型有紧密关系的一系列的集合和属性公开数据。当客户端程序试图从这些集合和属性中获取数据时,客户端库生成对应的HTTP请求并向服务发送这些请求。当服务返回数据时,客户端库将返回的AtomPub格式的数据转换成.NET Framework集合和类型,这样客户端程序就可以非常方便的使用这些集合与类型。
在下面的练习中,你将构建一个客户端程序,该程序连接到SalesData服务,然后执行使用IE访问服务时同样的请求。
练习:为SalesData服务创建一个测试客户端的程序
1. 在Visual Studio中,添加一个新的控制台程序工程到SalesData解决方案。并将其命名为SalesDataClient,该工程保存在WCF\Step.by.Step\Solutions\Chapter15\SalesData文件夹下。
2. 在解决方案浏览器窗口中,在SalesDataClient工程上点击右键,然后点击添加服务引用。在添加服务引用对话框中,点击自动探测。找到SalesData服务后,在命名空间字段处,输入SalesDataService,然后点击确认按钮。
粗略一看,添加数据Web服务引用对话框与添加SOAP Web服务引用一致。但是,Visual Studio能够识别数据Web服务,然后据此生成的客户端库匹配数据服务公开的数据,而不会匹配为SOAP Web服务操作对应的方法。
WCF数据服务的客户端库包含一个继承了DataServiceContext类型的类。该类有一个或多个DataServiceQuery类型的公共属性。该类的名字一般地与WCF数据服务依赖的实体模型ObjectContext对象的名字是一样的。比如,SalesDataService服务使用一个名为AdventureWorksEntities的ObjectContext对象连接到相关的实体模型,因此为客户端库生成的DataServiceContext类型的名字也是AdventureWorksEntities。
DataServiceContext类执行与实体框架中ObjectContext类相似的角色。客户端程序通过DataServiceContext对象连接到数据源,然后从数据服务所公布实体的DataServiceQuery属性中获取数据。每个DataServiceQuery属性都是一个generic集合,该集合呈现来自为WCF数据服务所提供的相关实体的数据。在SalesDataService服务中,实体模型提供了对AdventureWorks数据库中Contact,SalesOrderHeader和SalesOrderDetails数据表的读取。在客户端库中AdventureWorksEntities类中也包含了名为Contacts,SalesOrderHeader和SalesOrderDetails的DataServiceQuery类型的属性。
客户端库还提供每个DataServiceQuery集合所包含类型(Contact,SalesOrderHeader和SalesOrderDetail)的定义。客户端程序首先对DataServiceQuery集合执行LINQ查询,然后在客户端库中创建一个适当的HTTP请求从服务获取对应的数据。WCF数据服务获取匹配的数据后填充DataServiceQuery集合。客户端程序最后迭代该集合并从每个子项中获取数据。
3. 在文本编辑模式下打开SalesDataClient工程中的Program.cs文件,添加下面的声明到该文件的顶部:
4. 在Program类的main方法中,添加下面的代码:
该声明连接到SalesData服务。正如先前描述,AdventureWorksEntities类型是一个DataServiceContext对象,它充当向SalesData发送请求的代理。在构造函数中指定的URI为SalesData服务的地址。
5. 添加下面的代码到Main方法中
上述代码从SalesData服务获取订单的详细信息,并显示每个联系人的姓和名。请注意当你引用AdventureWorksEntities对象中的Contracts集合时,客户端库发送对应的HTTP GET请求(http://localhost:48000/SalesData/SalesDataService.svc/Contacts)至SalesData服务,然后填充Contracts集合。该请求与你使用IE具有同样的限制,该限制就是该请求也只仅仅返回前25个联系人的信息。
6. 添加下面的代码到Main方法中:
上述代码与上一步的代码基本相似,但是它的作用是查询SalesOrderDetails集合。该行为导致AdventureWorksEntities对象发送一个HTTP GET请求(http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderDetails)至SalesData服务,同样地,该请求仅仅返回前25条数据。
7. 添加下面的生命到Main方法中
上述代码在AdventureWorks数据库中从第51条记录开始获取25条SalesOrderDetails记录。
8. 添加下面代码到Main方法中:
上述代码获取根据单价排序后的SalesOrderDetails记录。
9. 添加下面代码到Main方法中
上述代码使用LINQ语法过滤和加工数据,列出客户编号为99的客户所有订单的SalesOrderID字段和TotalDue字段。
10. 添加下面代码到Main方法中:
上述代码片段获取订单和每个订单对应的联系人信息。
11. 重新生成解决方案。
现在你可以测试客户端程序。但是,在测试之前,为SalesData服务配置tracing是非常有用的。因为启用跟踪之后,你可以看到客户端程序向服务发送请求的详细信息。
练习:配置跟踪然后在客户端程序中测试SalesData服务
1. 在C:\...\SalesData工程中,使用WCF服务配置编辑器打开Web.config。
2. 在配置面板,点击诊断节点,在诊断面板中,点击启动跟踪链接。
3. 点击跟踪级别后的链接进入跟踪设置对话框,设置跟踪级别为Information,然后点击确认按钮。
4. 点击ServiceModelTraceListener链接。在侦听器设置对话框中,设置日志文件属性为web_tracelog.svclog,其路径为Solutions\WCF\Step.by.Step\Chapter15文件夹,然后点击确认按钮。
5. 保存配置文件并关闭WCF服务配置编辑器。
6. 在解决方案窗口,打开SalesData解决方案的属性窗口,设置C:\...\SalesData和SalesDataClient工程均为解决方案SalesData的启动工程,然后点击确认按钮。
7. 在非调适模式下启动解决方案。当IE出现后最小化IE。在客户端程序控制台窗口中,确认显示了前25个客户:
8. 按下ENTER键,然后确认前25条SalesOrderDetails继续出现。这些记录的订单编号从43659到43661(每个订单都包含了多个子项)。
9. 按下ENTER键,然后确认一个不同的SalesOrderDetial数据记录集显示在窗口中。该批订单的编号从43662到43666.
10. 再次按下ENTER键,SalesOrderDetails继续将按照UnitPrice字段升序排列。
11. 再次按下ENTER键,SalesOrderDetails继续将按照UnitPrice字段降幅排列。
12. 再次按下ENTER键,你将看到SalesOrderID和TotalDue显示在窗口中,订单编号从43682到69485。
13. 继续按下ENTER键,你将看到订单编号和每个订单对应客户名字的列表。
14. 按下ENTER键结束程序并关闭客户端控制台窗口。
15. 在Windows任务栏,在ASP.NET开发服务器上点击右键,然后点击停止。
16. 启动服务跟踪查看工具,然后打开Solutions\WCF\Step.by.Step\Chapter15文件夹下的web_tracelog.svclog。
17. 在服务跟踪查看工具中,点击行为标签,然后点击第一个名为"Process action"的行为。在右边面板中,选择"Received a message over a channel"项目。然后在右下面板中,滚动格式化标签对应内容的滚动条,直到找到To字段。该字段对应的URI为http://localhost:48000/SalesData/SalesDataService.svc/Contacts。该URI为客户端程序中的第一个测试生成。
18. 在行为面板, 第二个Process Action行为。在右边面板,点击"Received a request over a channel",然后在下面的面板中,确认To的值为 http://localhost:48000/SalesData/SalesDataService.svc/SalesOrderDetails
19. 重复上述过程,分别验证另外5个地址。
20.关闭服务跟踪查看器,然后返回到Visual Studio。
使用WCF数据服务修改数据
你可能期望构建客户端程序,通过WCF数据服务修改数据。客户端库实现了一个模型,使用该模型你可以在本地修改与服务对外公布的实体相关联 的数据,然后使用用于连接服务的DataServiceContext对象的SavaChanges方法成批地发送这些变动到服务。你需要注意,WCF数据服务必须显示地在InitializeService方法中指定合适的访问权限以允许对实体进行更改。下面的代码展示了如何对允许对Contacts实体进行更新。
接下来的代码为你展示了如何创建一个新的Contract实体对象。当你为WCF数据服务创建一个客户端库时,添加服务向导为每个实体类型生成一个静态的Create方法,该方法包含了使对象为非空的参数。向导还为每个实体生成了一个AddTo方法;你可以调用该方法以添加新创建的对象到DataServiceContext适当的集合对象中。最后调用SaveChanges方法提交更改到服务。
SaveChanges方法生成一个HTTP POST请求,将newContact的数据作为消息的body传递至服务。
如果修改一个对象,那么首先发送一个查询到WCF数据服务,更改需要更改的字段;然后调用DataServiceContext对象的UpdateObject方法通知更改;最后调用SaveChanges发送更改至服务。DataServiceContext对象生成一个HTTP PUT请求或者为每个更改的对象生成一个HTTP MERGE消息,然后把对象的数据作为消息的body传送给服务。比如下面的代码展示了如何更改客户编码为99的客户的邮件地址:
如果需要删除一个对象,那么与更新一个对象的方法相似。首先获取一个对象,然后调用DataServiceContext对象的DeleteObject方法,最后调用SaveChanges方法。SaveChanges方法生成一个HTTP DELETE消息,然后把该消息发送给WCF数据服务。下面的代码展示了如何删除一个客户编码为101的客户
请注意上面三个例子都调用了SaveChanges方法。如果你要在进行多次更改(插入,更新,删除,或者多个操作的联合),你仅仅需要在最后调用一次SaveChanges。DataServiceContext对象在其自身内部的一个可变集合中追踪所有对象的状态,然后针对每个修改生成对应的HTTP POST,PUT,MERGE和DELETE消息,然后再逐个发送这些消息到WCF数据服务。如果你要进一步优化,那么你可以在SaveChanges方法中指定SaveChangesOptions.ReplaceOnUpdate.Batch参数。该标记将会使DataServiceContext对象在一个单独的消息中发送所有的更改。这种方式减少了网络交通数量,并改善了客户端的性能。批处理还可以作为一个事务而运行,所有的操作要么成功要么失败。这对在应用程序中设计数据一致性非常有帮助。每个批请求对客户端产生一个单独的HTTP响应,该响应消息中包含了该批中所有操作的响应结果。如果任意一个操作失败,那么响应消息仅仅包含一个标明批操作失败的响应结果。
当使用任何多用户数据程序时,可能会导致并发异常,因此你需要在客户端中正确地处理并发。此外,你还需要确保在修改关联数据时保持数据的一致性。
在客户端段中处理异常
当一个客户端程序向WCF数据服务发送一条请求时,请求可能会因为多种原因而失败。比如,客户端程序可能试图访问未授权数据,或者执行了一个服务不允许的查询。
如果失败是由于客户端程序与服务交互的方式(与此相反的失败是由于比如网络失败等引起的其他原因),那么服务将抛出一个DataServiceException异常。DataServiceException类型是一个可序列化的异常,之所以这么设计是为了客户端能与WCF数据服务失败的原因进行通信。当客户端接收到一个DataServiceException异常,客户端库把该异常反序列化为一个DataServiceClientException对象,然后把该对象传递给客户端程序。
如果你的程序执行一个查询时发生异常,DataServiceClientException异常将被封装在DataServiceQueryException对象中,DataServiceClientExcaption的异常消息将为"执行请求时错误发生"。你可以读取DataServiceClientException异常对象,通过检查DataServiceQueryException对象的InnerExcpetion属性获取异常的原因。
如果客户端程序发送一个请求而不是一个查询,WCF数据服务发生异常时,服务将响应一个DataServiceRequestException异常。
客户端程序应当在执行查询时捕获DataServiceQueryException异常,执行其他类型(比如修改数据)的操作时捕获DataServiceRequestExcaption异常。客户端程序还应该捕获DataServiceClientException异常以处理WCF数据服务在执行其他类型操作时抛出的其他异常。