理解 OData v3 和 WCF 数据服务 5.x






4.86/5 (34投票s)
解释 OData 和 WCF Data Services,
目录
- 使用 POCO 创建 WCF 数据服务
- 幕后
- 通过 WCF 消息检查器启用 JSON 格式
- JSON light 是什么?
- WCF 数据服务 5.2.0 中的 JSON Light
- 使用服务操作和拦截器扩展数据服务
- 元数据、EDM 和 CSDL
- 使用 ETAG 进行并发更新
- 资源
本文内容
这不是一篇典型的“将实体框架模型公开为 WCF 数据服务”的文章;事实上,我特意在本文中省略了实体框架。请不要误解我的意思,我并不反对 EF,但是您会发现很多关于如何使用 EF 作为数据源设置 WCF 数据服务的优秀文章。
本文将讨论 OData v3 和 WCF 数据服务 5.0 和 5.2。当然,我不可能涵盖所有新功能或每个可用 API,也没有必要这样做;我将为您指出获取所需所有信息的资源。相反,我在这篇文章中的目的是讨论一些通常被 WCF 数据服务开发人员忽略的主题。
什么是 OData、Atom 和 AtomPub?
开放数据协议 (OData) 是一种规范数据公开和消费的协议。在数据以高速度公开且消费者连接到越来越多的数据端点的时代,客户端以通用方式访问这些端点非常重要。
OData 构建在 Http、Atom 和 JSON 等 Web 标准之上,提供对这些端点的基于 REST 的访问。数据作为实体公开,其中每个实体都可以被视为一个 Http 资源,因此它受 CRUD(创建、读取、更新、删除)操作的约束。
那么 OData 与 Atom 和 AtomPub 有何关系?
Atom 是一种公开 Feed 的方式,与 RSS 类似。如果您想知道 Atom 和 RSS 之间的区别,可以查看此网站 (http://en.wikipedia.org/wiki/Atom_(standard)#Atom_compared_to_RSS_2.0)
Atom 本身只允许 Feed 公开。如果您想发布数据,AtomPub(Atom 发布)提供了此功能。AtomPub 使用 Http 动词 GET、POST、PUT 和 DELETE 来实现数据发布。
OData 在 AtomPub 之上添加了一组扩展,以实现更高级和智能的操作,例如数据检索过滤和类型值定义。例如,下面是通过 OData 的强大功能实现的两个查询:
- http://server/service.svc/entity/$count
- http://server/service.svc/entity?$filter=(entityID add 4) eq 8
REST 与 SOAP:设计决策
你肯定已经预料到在 OData 文章中会有 REST 与 SOAP 的章节,不是吗?毕竟这个话题的资源确实稀缺。好吧,你明白这个笑话了。
如果你在网上搜索 REST 与 SOAP,你无疑会得到很多有价值的资源。所以,我不会以一种相当抽象的方式来讨论这个话题,而是会向你展示我在评估是选择 REST 还是 SOAP 时在工作中做出的真实世界决策。
我在电子政务部门工作(我为什么提到这一点很快就会清楚)。当我刚开始现在的工作时,第一个任务是创建一个集成服务总线,以消除任何点对点集成;当时这是主要情况。该总线还需要处理与互联网上其他电子政务机构的 B2B 通信。请看下面的图表,让我们看看每个要求是如何映射到(高级别)设计决策,即我应该使用 SOAP 还是基于 REST 的服务:
- 1、1.1 和 1.2:我们有一个最初的政府机构列表,我们需要允许它们查询我们的服务以进行数据检索。此外,我们知道随着时间的推移,机构列表会增加。因此,我们不希望与我们的合作伙伴共享不断扩展的端点列表。此外,由于政府业务的关键性质,如果我们要内部编辑我们的模式,我们希望停机时间最短。因此,我们最好的办法是有一个寻址机制,所有机构都获得相同的端点,具有几乎不变的固定模式,但根据每个请求的一些内容,我们将请求路由到内部托管的服务。这种情况最适合使用 SOAP WS-Addressing 扩展来处理。因此,我们最终实现了一个配置为使用 WS-Addressing 的 WCF 服务。(注意:使用 WS-Addressing 时,会遇到一些安全障碍,但这超出了本次讨论的范围)
- 2:另一个要求是,对于所有希望调用我们的服务总线的服务,都需要进行相互身份验证。当然,这在政府通信中非常典型。基于 REST 的服务通常通过 SSL 进行保护。对于任何形式的自定义客户端证书身份验证和数字签名,您最好使用 WS-Security,我们最终通过 WCF 的强大功能使用了它。
- 3:在内部,我们有一个 BPM 引擎,它从数据存储中请求数据。由于通信是内部的,我们对安全要求比较宽松。我们只需要用户名/密码身份验证,不需要加密或签名。在这种情况下,基于 REST 的服务因其简单性而是最佳选择。我们最终使用 WCF 数据服务构建了这些服务,它们很容易被大量基于 JQuery 的 BPM 前端消费,这使得 REST/JSON 组合成为一个完美的候选者。另一个重要的事情是有效载荷的大小。使用 REST 和 JSON,返回的有效载荷大小比 SOAP/XML 有效载荷的大小大大减小。
- 4:最后,我们需要 BPM 到被调用服务的事务支持。这需要 WS-Transaction SOAP 扩展,这使得 SOAP 成为必需品。基于 REST 的服务不支持事务。如果您想了解更多关于事务的信息,我建议您查看我的文章: https://codeproject.org.cn/Articles/35087/Truly-Understanding-NET-Transactions-and-WCF-Imple
从以上讨论可以看出,“SOAP vs. REST”这个问题的答案是众所周知的:“视情况而定”。希望上述场景能为您做出类似决策提供一个不错的起点。就我个人而言,只要有可能,我都会选择 REST 的简洁性;但有时,就是做不到。
OData 查询初探
首次了解 OData 的最佳方式是使用 OData 网站提供的一些演示服务。浏览到 http://www.odata.org/ecosystem 并向下滚动到“示例服务”部分。选择只读 OData 示例服务并点击浏览。
注意:OData 网站上提供的服务可能会随时间变化,但您在此处学到的技术基本保持不变。只有实体名称可能会更改。
现在,首次查看 OData 服务,让我们检查一些重要的观察结果
- 首先请注意,服务 URL 以“.svc”扩展名结尾。这表明它是一个 WCF 服务;更具体地说,它是一个 WCF 数据服务——我们将在稍后详细讨论。
- 另请注意,数据是通过 Atom feed 发布的。
- 另请注意,您获得的是服务提供的一组实体中的第一级
- 对于每个实体,您都会获得一个 href 属性,告诉您如何进一步查询子实体和相关实体
第 3 点和第 4 点证实,正如 REST 所承诺的那样,每个资源(实体)都被定义为一个 URI,易于消费。
现在让我们用这个服务玩一下。假设您想查询所有提供的产品。操作方法如下:
再次,我将讨论一些重要的突出显示项
- URL 指向 http://services.odata.org/OData/OData.svc/Products。回想一下,我们第一次查询中的“href=Products”告诉我如何查询所有产品。
- 每个产品都由一个条目表示,每个条目都有一个 ID,该 ID 是产品集中每个产品的 URI。例如,第一个产品的实体 ID 是 http://services.odata.org/OData/OData.svc/Products(0)。其中值“0”是下一点中讨论的实体主键。
- 产品实体的主键在 ID 元素中指定。此键唯一标识一个实体,在某些情况下它可以是一个复合键;例如,如果产品允许复合主键,它可能曾经是“/Products(0,1)”。
- 产品实体与其他实体存在关系;即类别和供应商。要查看键为 0 的产品的类别,您将使用“Products(0)/Category”,要检查同一产品的供应商,您将使用“Products(0)/Supplier”。
- 每个产品的属性列在 properties 元素中。
以下是一些更多查询
- http://services.odata.org/OData/OData.svc/Products(0) 将返回键为 0 的产品
- http://services.odata.org/OData/OData.svc/Products(0)/ID 返回 Rating 属性的值
- http://services.odata.org/OData/OData.svc/Products(0)/Category 返回产品的类别
关键词
您可以使用关键字执行一些高级查询操作。
例如,对于想要消费 OData 服务的客户端工具,可以使用以下方式公开元数据: http://services.odata.org/OData/OData.svc/$metadata。“metadata”关键字生成元数据,然后可以由代理生成工具使用。
另一个例子是“value”关键字,它剥离属性值周围的 XML 并以原始格式返回该值。例如,http://services.odata.org/OData/OData.svc/Products(1)/Price 返回用 Atom XML 包裹的价格:
使用“value”关键字返回原始格式
另一个有用的关键字是“filter”关键字。例如,以下查询返回所有价格 = 2.5 的产品:http://services.odata.org/OData/OData.svc/Products?$filter=Price%20eq%202.5 (注意:%20 编码由浏览器添加)
以下查询 http://services.odata.org/OData/OData.svc/Products?$filter=Price%20gt%202.5&$orderby=Price%20desc&$top=2 结合了多个关键字
- “filter”关键字使用“大于”运算符过滤价格 > 2.5 的产品
- 关键字“orderby”按降序排列价格
- 关键字“top”仅返回返回产品集中的前 2 个
格式化
到目前为止,我们一直使用 Atom XML 格式查询服务。Atom 的问题是响应的大小,因为它用 XML 标签包裹,这增加了整个有效载荷的大小。
然而,使用“format”关键字,您可以使用 JavaScript 对象表示法 (JSON) 格式返回数据
然而,您上面看到的是默认的“verbose JSON”。之所以称之为 verbose,是因为返回的 JSON 大小相当大(尽管比 Atom 小得多)。
OData V3 中的一个新功能是“light JSON”,它从 verbose JSON 中剥离不需要的信息,并呈现一个大大减小大小的 JSON。如果您再次检查 verbose JSON 数据,您会看到很多关于 OData 的信息。新的 light JSON 剥离了这些数据,只提供了一个元数据 URI,如果消费客户端关心被剥离的 OData 信息,他们可以进一步查询该 URI。
您可以使用“DataServiceVersion”头在请求头中指定是否需要 verbose 或 light JSON。如果您使用 Fiddler 检查使用 $format=JSON 关键字对 OData 服务的请求,您将看到“DataServiceVersion: 1.0”响应头:
然后,您可以使用 Fiddler composer 使用以下两个头来创建自定义的 light JSON 请求
请注意,在 Accept 标头中,我们提供了两组
- application/json;odata=light;q=1: 启用 JSON light 格式
- application/json;odata=verbose;q=0.5: 如果客户端不支持 JSON light,则回退到 verbose JSON 的选项。
现在回到 http://services.odata.org 网站上的 OData 服务。JSON light 可以通过实验链接访问,因为该功能尚未得到所有客户端的支持。导航到 http://services.odata.org/Experimental/OData/OData.svc/Products?$filter=Price%20gt%202.5&$orderby=Price%20desc&$top=2&$format=json 查看 JSON light 并将其大小与 verbose JSON 进行比较
那么何时使用 ATOM 和 XML 与 JSON 格式呢?答案通常取决于消费 OData 服务的客户端类型。JSON 通常最适合在用户代理上运行的客户端,例如基于 Ajax 和 JQuery 的客户端。ATOM XML 通常更适合调用 OData 服务的服务器端应用程序。
OData 客户端
OData 客户端是能够读取和解析 OData 服务返回数据的程序。其中一些客户端可以处理 ATOM 和 JSON 两种格式,而另一些则只能理解一种格式。
我们已经看到的一个客户端是浏览器。如前所述,我曾使用浏览器以 ATOM 和 JSON 两种格式从 OData 服务下载数据。然而,浏览器在实际应用程序中不太可能有用,因为客户端通常需要以更用户友好的格式(通常意味着以强类型方式)呈现数据,并根据这些数据执行进一步的处理和做出业务决策。
本节介绍其他类型的客户端。
Excel 2013 PowerPivot
Excel 有一个名为 PowerPivot 的加载项,它能够从 OData 服务读取 ATOM feeds。首先在 Excel 中找到 PowerPivot 加载项并执行以下操作
在结果窗口中,指向 http://services.odata.org/Experimental/OData/OData.svc/Products,测试连接并完成向导。这将从服务导入产品并以易于阅读的形式显示它们
据我所知,PowerPivot 无法读取 JSON 格式的数据。
Visual Studio 2012
稍后我们将在 Visual Studio 2012 中花费更多时间,但现在让我们先了解一下 VS 如何使用 OData 服务。
VS 2012 包含 WCF 数据服务客户端库,它通过熟悉的“添加服务引用”向导生成强类型客户端代理。
启动 VS 2012,创建一个新项目(可以是控制台或 Web),并调用“添加服务引用”向导。将向导指向我们演示服务的 URL http://services.odata.org/OData/OData.svc/。您将获得如下所示的顶级实体列表
点击“确定”关闭向导。
要检查强类型客户端代理,请从解决方案资源管理器工具栏中选择“显示所有文件”。然后打开 Reference.cs 文件。此文件将实体及其关系公开为强类型类和面向对象关系。您还将注意到代理已自动生成执行更新的方法。
其他客户端
您可以在此处查看 OData 客户端的完整列表:http://www.odata.org/ecosystem#consumers
通过 WCF 数据服务使用 OData
使用 POCO 创建 WCF 数据服务
有多种方法可以在 OData 服务中公开数据。以下是您可以使用 .NET 框架的方法
- 使用实体框架作为数据源的 WCF 数据服务
- 使用 LINQ to SQL 作为数据源的 WCF 数据服务
- 使用自定义 .NET 对象 (POCO) 的 WCF 数据服务
- WCF RIA 服务
- 如果上述所有选项都不适用,则使用自定义提供程序的 WCF 数据服务
普通旧 CLR 对象 (POCO) 是指遵循持久性无关设计原则的对象,该原则指出对象不包含任何持久性逻辑。简单来说,这些是您使用简单的面向对象机制创建的 CLR 对象。
让我们首先创建一个公开两个 CLR 对象的 WCF 数据服务。使用 VS 2012 创建一个新的 ASP.NET Web 应用程序。添加对“System.Data”的引用。创建一个新类并在其中添加以下代码:
- DataServiceKey 属性指定通过数据服务公开的每个实体的 ID
- DataServiceEntity 实际上将类标记为实体
- 课程和学生实体之间存在复合关系,其中每个学生有多门课程
- MyDataSource 类充当我的实体的容器,请注意它将实体公开为 IQueryable。这是必需的,以便 WCF 数据服务能够将类标识并公开为实体。
让我们检查这个类
- 它继承自 DataService 类
- 我们需要在“/* TODO: put your data source class name here */”处提供数据源类名。在我们的例子中,它将是持有 IQueryable 集合的“MyDataSource”类。
- 我们需要为服务中的实体设置访问规则。对于每个实体集,我们可以指定是否允许读取、更新或两者。在我们的例子中,我们希望允许对所有实体进行所有操作。所以这将是“config.SetEntitySetAccessRule("*", EntitySetRights.All);”。
- 同样,我们可以为可能定义的任何服务操作设置访问规则。服务操作将在稍后讨论。
- 最后,我们可以设置 OData 的协议版本,如本文撰写时所述,它处于 V3 版本。
因此,修改后的类将如下所示
现在运行应用程序,就这样,您已经创建了一个 WCF 数据服务
正如我们之前所做的那样,您可以开始查询服务并检查实体之间的关系。以下是一些查询示例
- https://:19865/MyDataService.svc/Courses
- https://:19865/MyDataService.svc/Students('01')/
- https://:19865/MyDataService.svc/Students('01')/Courses
- https://:19865/MyDataService.svc/Courses?&$top=1
幕后
所以很明显,WCF 数据服务实际上归根结底是一个 WCF 服务。然而,查看我们刚刚创建的示例的 web.config 文件,除了一个空白的 system.serviceModel 部分之外,没有看到任何与 WCF 相关的内容。
正如您所看到的,没有定义服务,也没有定义端点。WCF 数据服务使用默认配置来完成其工作,这些配置对您是隐藏的。那么这些配置是什么呢?最好的方法是直接向您展示。我已编辑 web.config 文件,内容如下:
我定义了一个名为“MyDataService”的服务,正如您所回忆的,它是我在前面的示例中创建的 WCF 数据服务的名称。
现在谈到 WCF 的核心:端点。如果您之前有 WCF 经验,您就知道端点通过地址、绑定和契约来标识。这里我提供了这些值。
绑定是 webHttpBinding,这是用于 REST 操作的 WCF 绑定
我将 REST 指定为我的地址,所以从现在开始,要查询我的服务,我将使用 https://:19865/MyDataService.svc/REST/ 来查询我的服务,而不是之前使用的 https://:19865/MyDataService.svc/。
最后,关于契约 System.Data.Services.IRequestHandler 是什么?契约是服务实现接口。在我们的例子中,服务是 MyDataService,提醒您,它的定义如下:
这引发了一个问题:为什么端点契约不是 DataService 而是 System.Data.Services.IRequestHandler?要回答这个问题,您需要了解 IRequestHandler 是什么。为此,我将使用 Reflector 向您展示此接口内部的内容。
现在检查 IRequestHandler 的内容,显示如下:
处理程序本身被定义为一个服务契约。它包含一个泛型方法,该方法接受一个泛型消息流并返回一个 Message 类型,这是 WCF 的泛型消息表示。此方法被定义并标记为 WCF 操作,并用 WebInvokeAttribute 标记,该属性接受任何 UriTemplate 和任何 Method。如果您是 .NET REST 编程新手,您可能会想:他刚刚说了什么鬼话?
好的,在 WCF 数据服务之前,如果您想创建基于 REST 的 WCF 服务,您必须使用 REST 启动工具包。这样做时,您必须手动定义 WCF 数据服务抽象掉的所有内容。您必须做的事情之一是使用以下两个属性之一标记您想要通过 REST 公开的任何操作
- WebGetAttribute:用于使用 Get Http 动词公开操作。只能用于检索数据。
- WebInvokeAttribute:用于通过任何所需的 Http 动词公开操作。它默认为 POST,但可以使用 Method 参数指定任何所需的动词。因此,WebInvokeAttribute 可用于数据更新和检索(注意:通常使用 WebGet 进行检索,使用 WebInvoke 进行更新以与 Http 规范保持一致,但是,当使用 WebInvoke(Method=”GET”) 时,生成的元数据与 WebGet 的元数据相似)
UriTemplate 允许您为方法指定 REST URI。回想一下,在 REST 中,每个资源都可以使用唯一的 URI 指定。UriTemplate 就是为特定 WCF 操作(在这种情况下是资源)实现此目的的方法。
最后一点;在 Reflector 中检查处理程序的派生类型
您会注意到 DataService
总结一下,IRequestHandler 是 WCF Data Service 中所有服务内操作的通用处理程序。这消除了我们手动定义 REST 启动工具包中过去需要定义的内容的需求。
最后,使用新的 URL 浏览服务
您现在可以像往常一样导航服务,例如
- https://:19865/MyDataService.svc/REST/Students
- https://:19865/MyDataService.svc/REST/Students('01')
- https://:19865/MyDataService.svc/REST/Students('01')/Courses
- https://:19865/MyDataService.svc/REST/Courses?&$top=1
通过 WCF 消息检查器启用 JSON 格式
当我向您展示 odata.org 网站提供的演示服务时,我能够使用“format”关键字以 JSON 格式显示数据,而不是默认的 ATOM 格式。您可能会想在我们的创建服务上应用相同的技术,例如
https://:19865/MyDataService.svc/REST/Students?&$format=json
然而,这样做并不会得到你所期望的结果。相反,你将得到:
您收到此响应的原因是(在本文撰写时),WCF 数据服务默认不提供隐式告诉您的服务以 JSON 格式公开数据的方式。
我们需要一种方法来拦截客户端请求并显式地将 Accept 标头设置为“application/json”,以便 WCF 数据服务运行时知道生成 JSON 响应而不是 ATOM 响应。基本上有两种方法可以做到这一点,要么通过 Http 模块,要么通过 WCF 消息检查器。我更喜欢第二种方法。
要完全理解 WCF 通道层和扩展点,我建议您阅读我的这篇文章
http://thedotnethub.blogspot.com/2012/01/creating-soaprest-based-wcf-service.html
简而言之,WCF 提供了扩展点,您可以在客户端和服务端拦截请求。在此示例中,我将在服务端创建一个扩展点以添加所需的 JSON 标头。
步骤 1:创建消息检查器和行为扩展
public class JSONMessageInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
{
HttpRequestMessageProperty requestMessage = request.Properties["httpRequest"] as HttpRequestMessageProperty;
UriTemplateMatch jsonMatch = (UriTemplateMatch)request.Properties["UriTemplateMatchResults"];
if (jsonMatch.QueryParameters["$format"] != null && jsonMatch.QueryParameters["$format"].ToLower() == "json")
{
jsonMatch.QueryParameters.Remove("$format");
requestMessage.Headers["Accept"] = "application/json;odata=light,application/json;odata=verbose"; //do not worry. This will be //explained in the next section
} }
public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState) { }
}
public class JSONEndpointBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) { }
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { }
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new JSONMessageInspector());
}
public void Validate(ServiceEndpoint endpoint) { }
}
public class JSONBehaviorExtensionElement : BehaviorExtensionElement
{
public JSONBehaviorExtensionElement() { }
public override Type BehaviorType
{
get { return typeof(JSONEndpointBehavior); }
}
protected override object CreateBehavior()
{ return new JSONEndpointBehavior(); }
}
AfterReceiveRequest 方法包含重要的逻辑。它检查传入请求并检查 $format 是否设置为“json”。如果是,它强制 accept 标头为“application/json”,以指示 WCF 返回 JSON 格式的响应,并从查询参数列表中删除 $format,以防止出现前面描述的“查询参数'$format'以系统保留的'$'字符开头但无法识别”问题。
再次,请参阅我的帖子以了解这背后的机制,因为我希望本文重点关注 OData 而不是 WCF 架构。
步骤 2:在 web.config 中注册扩展点
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="messageInspector" type="JSONBehaviorExtensionElement, TestOData, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<behaviors>
<endpointBehaviors>
<behavior name="JSONBehavior">
<messageInspector/>
<webHttp helpEnabled="true" />
</behavior>
</endpointBehaviors>
</behaviors>
<services>
<service name="MyDataService">
<endpoint binding="webHttpBinding" contract="System.Data.Services.IRequestHandler" address="REST" behaviorConfiguration="JSONBehavior"></endpoint>
</service>
</services>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
</system.serviceModel>
现在运行应用程序并浏览到 https://:19865/MyDataService.svc/REST/Students。您将看到 Atom 格式的响应
现在使用 https://:19865/MyDataService.svc/REST/Students?&$format=json 指示应用程序使用 JSON 格式。您将获得 JSON 格式的响应
JSON light 是什么?
如前所述,JSON light 是 OData 针对冗余 JSON 的新方向。有关更多详细信息,请参阅“格式化”部分。
回到上一节,我漏掉了一个细节。再次检查这行代码
requestMessage.Headers["Accept"] = "application/json;odata=light,application/json;odata=verbose";
为什么我没有简单地使用:requestMessage.Headers["Accept"] = "application/json"?
答案在于 MyDataService.svc.cs 类中的以下行
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V3;<o:p> </o:p>
V3 表示我正在使用 OData V3,当 Accept 标头设置为“application/json”时,它默认为 JSON light。要实际查看此内容,请编辑消息检查器中的 accept 标头,使其显示为
requestMessage.Headers["Accept"] =
"application/json";
现在浏览到 https://:19865/MyDataService.svc/REST/Students?&$format=json
我得到“不支持的媒体类型请求”的原因是我的客户端(WCF 客户端库客户端 5.0)尚不支持 JSON light。
现在编辑 MyDataService.svc.cs 中的 MxProtocolVersion,使其显示为
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V3;
再次浏览到 https://:19865/MyDataService.svc/REST/Students?&$format=json:
这次 JSON 格式被渲染,因为我们使用的是 OData V2,它默认为冗长 JSON。
这就是为什么在使用 DataServiceProtocolVersion.V3 时,我将 accept 标头设置为 "application/json;odata=light,application/json;odata=verbose";其中 verbose JSON 作为客户端不支持 JSON light 时的备用选项。
WCF 数据服务 5.2.0 中的 JSON Light
数据服务 5.2.0(包括服务和客户端)的最新版本于几天前发布。让我们使用 NuGet 安装这两个包
版本 5.2.0 完全支持 JSON light。您可能还记得上一节“JSON light 是什么?”中提到的,当时我使用的是 WCF 数据服务客户端 V5.0,它不支持 JSON light。然后我使用了一个自定义 Http 标头来支持冗长 JSON 作为客户端不支持 JSON light 时的备用选项。
现在更新到 V5.2.0 后,让我们移除消息检查器:
现在浏览到 https:///TestOdata/MyDataService.svc/REST/Courses?&$format=json。您将看到返回的 JSON light,甚至无需发出自定义标头:
这使得 JSON light 在 V5.2.0 中成为一级公民。
使用服务操作和拦截器扩展数据服务
正如我们所看到的,WCF 数据服务将数据作为实体公开给客户端。但是,有时您希望公开由自定义逻辑塑造的数据;例如,一个实体是其他实体合并的结果。服务操作允许您向客户端公开自定义操作。
您可能还希望在 WCF 数据服务处理请求之前拦截请求并执行一些逻辑。在这种情况下,拦截器就会发挥作用。
服务操作
要向我们的示例添加服务操作,只需将以下方法添加到 MyDataService 类
[WebGet]
public IQueryable<Student> GetCustomLogicStudents()
{
//simulate some logic. here i will just return the same result set
return (new List<Student>()
{
new Student { NationalID = "01", Name = "Mohamad", Courses = null}
}).AsQueryable();
}
请注意,您需要将 WebGetAttribute 应用于此操作。同样,您也可以将 WeInvokeAttribute 应用于执行更新的服务操作(有关 WebGet 和 WebInvoke 的更多信息,请参阅“幕后”部分)。
您还需要按如下方式访问新的服务操作
config.SetServiceOperationAccessRule("GetCustomLogicStudents", ServiceOperationRights.All);
现在浏览到 https://:19865/MyDataService.svc/REST/GetCustomLogicStudents 查看服务操作的实际效果。
查询拦截器
要添加查询拦截器以阻止返回 ID 为 1 的课程,请将以下方法添加到 MyDataService 类
[QueryInterceptor("Courses")]
public
Expression<Func<Course, bool>> CoursesQuery()
{
return
p => p.CourseID != "1";
}
现在,如果您查询 https://:19865/MyDataService.svc/REST/Courses,您会看到 ID 为 1 的课程将不会被返回。
元数据、EDM 和 CSDL
在“关键词”部分中,我提到您可以使用“$metadata”来生成 WCF 数据服务的元数据。让我们为我们的示例服务这样做
Edmx 和 DataServices 元素下的 Schema 元素是概念模式定义语言 (CSDL) 规范。CSDL 定义了 OData 的数据模型,即 Microsoft 的实体数据模型 (EDM),它是实体关系模型的派生。这会将 CSDL 数据公开为一组实体以及它们之间的关联(关系);我们从文章开头就开始使用这些。
是的,我知道你在想什么,那和实体框架的 edmx 是一样的;然而,在此上下文中,这与实体框架无关。
正是 CSDL,Visual Studio 2012 的“添加服务引用”对话框用来生成“Reference.cs”文件,即强类型客户端代理。
让我们继续使用 VS 向我们的示例服务添加服务引用。使用 CSDL,对话框能够强类型识别应生成的类:
如果您检查生成的 Reference.cs 文件,您会看到生成的类的名称是“MyDataService”,这是 CSDL 的“Namespace”属性的值。
使用 ETAG 进行并发更新
考虑以下场景:客户端应用程序正在调用我们的 WCF 数据服务。它首先检索一个特定的 Course 对象,更新它,然后将更改提交回服务。现在假设在客户端检索 Course 对象和提交更改之间,服务端的 Course 对象被更新了。这会导致并发问题,因为客户端在执行更新时可能根据它从服务获得的最后一个 Course 对象做出了决策;然而,这个对象在客户端不知情的情况下发生了变化。这可能会导致各种业务问题。
处理并发有两种方式;要么乐观,要么悲观。乐观就像说“好吧,我们暂时不理会并发,让客户端更新 Customer 对象,如果出现问题我们再担心”。然而,悲观是过度保护,就像说“宁可信其有,不可信其无。如果 Course 对象在服务端的价值发生了变化,我希望阻止客户端更新它”。
当您希望以悲观的方式处理并发问题时,ETAGs 提供了检查并发问题的机制。
为了看到实际效果,我将对我们的 Course 类进行一些更新。以下是完整的代码
[DataServiceKey("CourseID")] [DataServiceEntity] [ETag("CourseNum")] public class Course { public decimal CourseID { get; set; } public int CourseNum { get; set; } public static List<Course> GetCourses() { return (new List<Course>() { new Course() { CourseID=1, CourseNum=2}, new Course() { CourseID=2, CourseNum=1} }); } } public class MyDataSource:IUpdatable { static List<Course> courses; static List<Student> students; static MyDataSource() { courses = Course.GetCourses(); students = Student.GetStudents(); } public IQueryable<Course> Courses { get { return (courses.AsQueryable()); } } public IQueryable<Student> Students { get { return (students.AsQueryable()); } } #region IUpdatable public object CreateResource(string containerName, string fullTypeName) { throw new NotImplementedException(); } public void DeleteResource(object targetResource) { throw new NotImplementedException(); } public object GetResource(IQueryable query, string fullTypeName) { object result = null; var enumerator = query.GetEnumerator(); while (enumerator.MoveNext()) { if (enumerator.Current != null) { result = enumerator.Current; break; } } if (fullTypeName != null && !fullTypeName.Equals(result.GetType().FullName)) { throw new DataServiceException(); } object resultClone = result; //TODO: Uncomment this line to see ETAG in action //((Course)resultClone).CourseNum = 100; return resultClone; } public object GetValue(object targetResource, string propertyName) { var targetType = targetResource.GetType(); var targetProperty = targetType.GetProperty(propertyName); return targetProperty.GetValue(targetResource, null); } public void SetValue(object targetResource, string propertyName, object propertyValue) { Type targetType = targetResource.GetType(); PropertyInfo property = targetType.GetProperty(propertyName); property.SetValue(targetResource, propertyValue, null); } public object ResolveResource(object resource) { return resource; } public void SaveChanges() { // no persistent medium } public void SetReference(object targetResource, string propertyName, object propertyValue) { throw new NotImplementedException(); } public object ResetResource(object resource) { throw new NotImplementedException(); } public void ClearChanges() { throw new NotImplementedException(); } public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded) { throw new NotImplementedException(); } public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved) { throw new NotImplementedException(); } #endregion }
首先要注意的是,我用 ETAG 属性装饰了 Course 类,并将 CourseNum 设置为要监控的值(稍后会详细介绍)。
接下来,我让自定义数据源实现 IUpdatable。这是必需的,以便我的数据源接受来自客户端的更新(如果客户端只想查询我的服务,则我的数据源上不需要进行更新)。
现在创建一个控制台应用程序并将服务引用添加到服务中
https:///TestOdata/MyDataService.svc/REST/
MyDataSource proxy = new MyDataSource(new Uri("https:///TestOdata/MyDataService.svc/REST/"));
Course course = (from c in proxy.Courses
where c.CourseID == 1
select c).First();
course.CourseID = 2;
proxy.UpdateObject(course);
DataServiceResponse res = proxy.SaveChanges();
该代码首先检索一个特定的 Course 对象,更新它,然后将更新发送回服务。运行客户端并检查 Fiddler
在这里您可以看到,在请求中,“If-Match”标头表示它将检查 ETAG 值是否为 2。响应实际上带有 ETAG 值 2,因此调用成功。
现在让我们耍个花招。回到 MyDataSource 类的代码,取消注释以下行
//((Course)resultClone).CourseNum = 100;
在这行代码中,我硬编码了一个对 CourseNum 属性的更改(因为我没有像 EF 那样的持久化介质)。这样,客户端将尝试更新的对象将与它最初检索到的对象不同。
再次运行客户端。检查 Fiddler:
这次调用失败,并抛出异常“请求头中的 etag 值与对象的当前 etag 值不匹配”。
资源
WCF Web Http 编程模型
http://msdn.microsoft.com/zh-cn/library/bb412169.aspx
MSDN 上的 WCF 数据服务 5.0
http://msdn.microsoft.com/zh-cn/library/hh487257(v=vs.103).aspx