数据点

一个创建 OData 的新选项: Web API

Julie Lerman

下载代码示例

早在 OData 规范出现以前,Microsoft .NET 开发人员就已能够创建 OData 源。借助 WCF 数据服务,可使用具象状态传输 (REST) 在 Web 上公开实体数据模型 (EDM)。换句话说,可经由以下 HTTP 调用使用这些服务: GET、PUT、DELETE 等。随着创建这些服务的框架的发展(中途数次更改名称),输出也在不断演变,并最终形成以 OData 规范 (odata.org) 封装的形态。目前已出现多种可使用 OData 的客户端 API,来源如 .NET、PHP、JavaScript 以及其他众多客户端。但直到最近,能够轻松创建服务的唯一方式仍是借助 WCF 数据服务。

WCF 数据服务是一项 .NET 技术,它可对 EDM(.edmx,或通过 Code First 定义的模型)进行简单的封装然后公开该模型,以供经由 HTTP 查询和更新。由于这些调用采用 URI 形式(如 http://mysite.com/mydataservice/­Clients(34)),因此,您甚至可以通过 Web 浏览器或 Fiddler 之类的工具执行查询。为方便创建 WCF 数据服务,Visual Studio 提供了一个项目模板,可用来构建使用一组 API 的数据服务。

现在又出现了一种创建 OData 源的新方式 — 利用 ASP.NET Web API。在本文中,我将概述这两种方法之间的某些区别,并就如何在两者间作出选择给予指导。此外,我还将介绍某些与创建 Web API 不同的 OData API 创建方式。

API 对数据服务(宏观角度)

WCF 数据服务是一种 System.Data.Services.DataService,它包装已定义的 ObjectContext 或 DbContext。在声明该服务类时,它是包含上下文的泛型 DataService(即 DataService<MyDbContext>)。它在初始时呈完全锁定状态,因此,应在需要公开该服务的上下文的 DbSets 构造函数中设置访问权限。这就是您需要完成的全部操作。其余工作都由底层的 DataService API 负责处理: 在对该服务的客户端应用程序 HTTP 请求的响应中直接与上下文交互,以及查询和更新数据库。另外,也可以向该服务添加一些自定义设置,重写其部分查询或更新逻辑。但在大多数情况下,其目的在于让 DataService 负责与上下文的大部分交互工作。

另一方面,可借助 Web API 在对 HTTP 请求(PUT、GET 等)的响应中定义上下文交互。由 API 公开方法,由您定义方法逻辑。您不必与实体框架乃至数据库进行交互。您可以拥有客户端正在请求或发送的内存中对象。访问点不会像在使用 WCF 数据服务时那样神奇地创建出来;相反,需要您来控制如何响应这些调用。这是选择服务而非 API 来公开 OData 的决定性因素。如果需要公开的大部分操作只是简单的 Create、Read、Update、Delete (CRUD) 而无需进行大量自定义设置,那么数据服务将是您的最佳选择。如果需要自定义大量行为,那么使用 Web API 更为适宜。

我很赞同 Microsoft Integration MVP Matt Milner 在最近一次聚会上的阐述方式: “WCF 数据服务适合于从数据和模型开始,并且只希望公开它们的情形。而从 API 开始且需要定义应公开哪些内容时,使用 Web API 会更合适。”

使用标准 Web API 打下基础

我发现,对于缺乏 Web API 使用经验的新手来说,在了解新的 OData 支持之前,最好先学习一些 Web API 的基础知识,然后弄清它们与创建 Web API(用于公开 OData)有何关联。在本文中,我将这样做:首先创建一个使用实体框架作为其数据层的简单 Web API,然后将其转换为提供 OData 形式的结果。

Web API 的用途之一是作为模型-视图-控制器 (MVC) 应用程序中标准控制器的替代品,可将其创建为 ASP.NET MVC 4 项目的一部分。如果不需要前端,则可从空的 ASP.NET Web 应用程序开始,然后添加 Web API 控制器。但是,为了照顾新手,我将从 ASP.NET MVC 4 模板开始(因为它提供了会生成部分起始代码的基架)。当您了解各部分是如何结合在一起的之后,就可以直接从空项目开始工作了。

因此,我将创建一个新的 ASP.NET MVC 4 应用程序,然后在系统提示时选择空模板(不是 Web API 模板,Web API 模板是专为使用视图的更强大应用程序而设计的,在这里就大材小用了)。这将生成一个 Models、Views 和 Controllers 均为空文件夹的 MVC 应用程序项目组织结构。图 1 将空模板和 Web API 模板生成的结果进行了对比。您会发现,空模板生成的组织结构要简单得多,我所需要做的只是删除几个文件夹。


图 1 使用空模板和 Web API 模板生成的 ASP.NET MVC 4 项目

我也不需要 Models 文件夹,因为我将使用现有的域类集合和独立项目中的 DbContext 来提供模型。接下来,我将借助 Visual Studio 工具创建第一个控制器,它是一个 Web API 控制器,用于与从我的 MVC 项目引用的 DbContext 和域类进行交互。我的模型包括 Airline、Passengers、Flights 类及与航空公司有关的一些其他数据类型。

由于使用的是空模板,所以需要添加一些引用(分别是对 System.Data.Entity.dll 和 EntityFramework.dll 的引用),以便调用 DbContext。您可以通过安装 EntityFramework NuGet 包来添加这两个引用。

可以通过与创建标准 MVC 控制器相同的方式创建新的 Web API 控制器: 在解决方案中右键单击 Controllers 文件夹,然后依次选择“添加”、“控制器”。此时会显示用于创建具有 EF 读取和写入操作的 API 控制器模板(如图 2 所示)。除此以外,还有一个空 API 控制器。我们从 EF 读取/写入操作开始,以便与用于 OData 的控制器(也将采用实体框架)进行对比。


图 2 一个用于创建包含预填充操作的 API 控制器的模板

如果您之前创建过 MVC 控制器,就会发现所生成的类是相似的,只不过提供的不是一组视图相关的操作方法(如 Index、Add 和 Edit),而是一组 HTTP 操作罢了。

例如,这里有两个 Get 方法(如图 3 所示)。第一个方法 Get­Airlines 的签名不接受任何参数,并且使用 AirlineContext 实例(模板基架将之命名为 db)返回一个可枚举的 Airline 实例集合。另一个方法 GetAirline 接受一个整型参数,然后利用该参数查找并返回一个特定的航空公司。

图 3 一些由 MVC 基架创建的 Web API 控制器方法

 

public class AirlineController : ApiController   {     private AirlineContext db = new AirlineContext2();     // GET api/Airline     public IEnumerable<Airline> GetAirlines()     {       return db.Airlines.AsEnumerable();     }     // GET api/Airline/5     public Airline GetAirline(int id)     {       Airline airline = db.Airlines.Find(id);       if (airline == null)       {         throw new HttpResponseException           (Request.CreateResponse(HttpStatusCode.NotFound));       }       return airline;     }

模板添加了注释,以演示如何使用这些方法。

向 Web API 提供一些配置之后,可以在我的应用程序分配的端口上使用示例语法在浏览器中直接检视其操作: http://localhost:1702/api/Airline。 这是默认的 HTTP GET 调用,因此,由应用程序路由以执行 GetAirlines 方法。 Web API 通过内容协商来确定如何格式化结果集。 我的默认浏览器是 Google Chrome,其触发的协商结果是 XML 格式。 来自客户端的请求控制结果的格式。 例如,Internet Explorer 不会发送任何与其接受何种格式有关的特定标头信息,因此,Web API 将默认返回 JSON。 图 4 显示我收到的 XML 结果。

图 4 Airline WebAPI 对 GET 的响应信息,在我的浏览器中显示为 XML

<ArrayOfAirline xmlns:i=http://www.w3.org/2001/XMLSchema-instance  xmlns="http://schemas.datacontract.org/2004/07/DomainClasses">     <Airline>       <Id>1</Id>       <Legs/>       <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>       <Name>Vermont Balloon Transporters</Name>     </Airline>     <Airline>       <Id>2</Id>       <Legs/>       <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>       <Name>Olympic Airways</Name>     </Airline>     <Airline>       <Id>3</Id>       <Legs/>       <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>       <Name>Salt Lake Flyer</Name>     </Airline> </ArrayOfAirline>

遵照 GetAirline 方法的指导信息,如果在请求中添加一个整型参数,如 http://localhost:1702/api/Airline/3,则只返回键值 (ID) 为 3 的航空公司:

<Airline xmlns:i=http://www.w3.org/2001/XMLSchema-instance   xmlns="http://schemas.datacontract.org/2004/07/DomainClasses">     <Id>3</Id>     <Legs/>     <ModifiedDate>2013-02-22T00:00:00</ModifiedDate>     <Name>Salt Lake Flyer</Name> </Airline>

如果使用的是可以明确控制到 API 的请求的工具(如 Internet Explorer 或 Fiddler)以确保得到 JSON 格式的结果,则对于 ID 为 3 的 Airline 的请求结果将以 JSON 格式返回:

{"Id":3,   "Name":"Salt Lake Flyer",   "Legs":[],   "ModifiedDate":"2013-03-17T00:00:00" }

这些响应包含对航空公司类型的简单表述,每一属性具有以下元素: ID、Legs、ModifiedDate 和 Name。

此外,该控制器还包含 PutAirline 方法,Web API 在响应 PUT HTTP 请求时会调用该方法。 PutAirline 包含使用 AirlineContext 更新航空公司的代码。 除此以外,还有用于插入操作的 PostAirline 方法和用于删除操作的 DeleteAirline 方法。 这些操作无法在浏览器 URL 中演示,但您可在 MSDN、Pluralsight 等处找到大量 Web API 的入门资料,因此,我将转到下一个话题:将其转换为输出符合 OData 规范的结果。

将 Web API 转变为 OData 提供程序

现在,您已基本了解借助 Web API 使用实体框架公开数据的方法,下面我们来讲解 Web API 的特殊用法:从数据模型创建 OData 提供程序。 您可以通过将控制器转变为 OData 控制器(使用 ASP.NET 和 Web Tools 2012.2 包中提供的类)然后重载其 OData 特有方法,强制 Web API 返回 OData 格式的数据。 有了这种新的控制器类型,您甚至不需要由模板创建的方法。 事实上,对于创建 OData 控制器,更高效的途径是选择空 Web API 基架模板(而非创建 CRUD 操作的那个)。

要实现这一转换,需要执行四个步骤:

  1. 使控制器成为 ODataController 类型并实现其 HTTP 方法。 这步我会走个捷径。
  2. 在项目的 WebAPIConfig 文件中定义可用的 EntitySets。
  3. 在 WebAPIConfig 中配置路由。
  4. 将控制器类的名称改为复数形式,以符合 OData 约定。

创建 ODataController 我将使用派生自 ODataController 的 EntitySetController 而非从 ODataController 直接继承,以便借助一系列虚拟 CRUD 方法提供更高级别的支持。 我使用了 NuGet 来安装 Microsoft ASP.NET Web API OData 包,以获取包含这两个控制器类的适当程序集。

我的类初始时是下面这样,它现在从 EntitySetController 继承,并指定将该控制器用于 Airline 类型:

public class AirlinesController : EntitySetController<Airline,int> {   private AirlineContext db = new AirlineContext();   public override IQueryable<Airline> Get()   {     return db.Airlines;   }

我已重载了 Get 方法,以返回 db.Airlines。 请注意,我没有对 Airlines DbSet 调用 ToList 或 AsEnumerable。 Get 方法需要返回 IQueryable 类型的 Airline,这将由 db.Airlines 实现。 这样,OData 的使用者可以定义对该集合的查询,并随后在数据库上执行,而不必将所有 Airlines 抓进内存,然后进行轮询。

可以重载和添加逻辑的 HTTP 方法有 GET、POST(用于插入)、PUT(用于更新)、PATCH(用于合并更新)和 DELETE。 但对于更新,实际上将使用虚拟方法 CreateEntity 来重载针对 POST 调用的逻辑,UpdateEntity 用于针对 PUT 调用的逻辑,PatchEntity 用于处理 PATCH HTTP 调用所需的逻辑。 可用作该 OData 提供程序一部分的其他虚拟方法有: CreateLink、DeleteLink 和 GetEntityByKey。

在 WCF 数据服务中,通过配置 SetEntitySetAccessRule 来按每个 EntitySet 控制允许的 CRUD 操作。 但在使用 Web API 时,只需添加需要支持的方法,并忽略不想让使用者访问的方法即可。

为 API 指定 EntitySets Web API 需要知道应使哪些 EntitySets 对使用者可用。 起初,我对这条规则感到困惑。 我本以为它能够通过读取 AirlineContext 自行发现要公开的 EntitySets。 但经过反复思索,我发现这与在 WCF 数据服务中使用 SetEntitySetAccessRule 很相似。 在 WCF 数据服务中,需要在公开特定集合时定义允许的 CRUD 操作。 但在使用 Web API 时,首先需要修改 WebApiConfig.Register 方法,以指定将成为 API 一部分的集合,然后使用控制器中的方法公开特定的 CRUD 操作。 通过 ODataModelBuilder 指定集合 — 这与 DbContext.ModelBuilder 类似(您可能在使用 Code First 时用到过)。 下面是 WebApiConfig 文件中 Register 方法的代码,以使 OData 源公开 Airlines 和 Legs:

ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();                   modelBuilder.EntitySet<Airline>("Airlines");                   modelBuilder.EntitySet<FlightLeg>("Legs");

定义查找 OData 的路由 接下来,Register 方法需要一个指向该模型的路由,以便在调用 Web API 时,提供对所定义的 EntitySets 的访问:

Microsoft.Data.Edm.IEdmModel model = modelBuilder.GetEdmModel(); config.Routes.MapODataRoute("ODataRoute", "odata", model);

您会发现,许多演示使用“odata”作为 RoutePrefix 的参数用于定义 API 方法的 URL 前缀。 这虽然是个不错的标准,但您可以为其指定任意名称。

为了证明这一点,我将它更改为:

config.Routes.MapODataRoute("ODataRoute", "oairlinedata", model);

重命名控制器类 对于控制器,应用程序模板生成的代码将采用单数命名约定,例如 AirlineController 和 LegController。 但是,OData 的侧重点是 EntitySets,因而通常采用实体名称的复数形式进行命名。 由于 EntitySets 已为复数,所以只需将控制器类的名称更改为 AirlinesController,以与 Airlines EntitySet 一致。

使用 OData

现在,我可以使用熟悉的 OData 查询语法操作 API 了。 我将首先使用下面的请求来请求一个包含可用内容的列表: http://localhost:1702/oairlinedata/。 结果如图 5 所示。

图 5. 请求包含可用数据的列表。

http://localhost:1702/oairlinedata/ <service xmlns="http://www.w3.org/2007/app" xmlns:atom=   "http://www.w3.org/2005/Atom" xml:base="http://localhost:1702/oairlinedata /">     <workspace>       <atom:title type="text">Default</atom:title>       <collection href="Airlines">         <atom:title type="text">Airlines</atom:title>       </collection>       <collection href="Legs">         <atom:title type="text">Legs</atom:title>       </collection>     </workspace> </service>

结果显示该服务公开了 Airlines 和 Legs。 接下来,我将通过 http://localhost:1702/oairlinedata/Airlines 请求一份 OData 形式的 Airlines 列表。 OData 可以 XML 或 JSON 格式返回。 Web API 结果的默认格式为 JSON:

{   "odata.metadata":     "http://localhost:1702/oairlinedata/$metadata#Airlines","value":[     {       "Id":1,"Name":"Vermont Balloons","ModifiedDate":"2013-02-26T00:00:00"     },{       "Id":2,"Name":"Olympic Airways","ModifiedDate":"2013-02-26T00:00:00"     },{       "Id":3,"Name":"Salt Lake Flyer","ModifiedDate":"2013-02-26T00:00:00"     }   ] }

OData URI 的众多功能之一是查询。 默认情况下,Web API 未启用查询,因为这会在服务器上产生额外的负荷。 因此,在向适当的方法添加 Queryable 注释之前,将无法使用 Web API 的这些查询功能。 例如,我向 Get 方法添加了 Queryable(如下所示):

[Queryable] public override IQueryable<Airline> Get() {   return db.Airlines; }

现在,将可以使用 $filter、$inlinecount、$orderby、$sort 和 $top 方法。 下面是一个使用 OData 筛选器方法的查询:

http://localhost:1702/oairlinedata/Airlines?$filter=startswith(Name,'Vermont')

ODataController 允许限制查询,以防使用者导致服务器出现性能问题。 例如,可以限制在单次响应中返回的记录数。 有关更多信息,请参阅针对 Web API 的“OData 安全指南”文章 (bit.ly/X0hyv3)。

这只是冰山一角

我只讨论了可借助 Web API OData 支持提供的查询功能的一部分。 还可以使用 EntitySetController 的虚拟方法,以实现对数据库的更新。 除了 PUT、POST 和 DELETE 以外,另一个有趣的操作是 PATCH:对于只有少量字段发生更改的更新,可以通过它发送明确而高效的请求,而非发送进行 POST 所需的完整实体。 但是,PATCH 方法中的逻辑需要处理适当的更新,在使用实体框架时,这通常意味着从数据库检索当前对象,然后使用新值进行更新。 如何实现该逻辑取决于了解需要在工作流程中的哪个时间点负担将数据推送上线的开销。 另一个需要一提的重点是,该版本(使用 ASP.NET 和 Web Tools 2012.2 包)仅支持 OData 功能的一个子集。 也就是说,并非所有可对 OData 源作出的 API 调用都适用于使用 Web API 创建的 OData 提供程序。 ASP.NET 和 Web Tools 2012.2 包的发行说明列出了支持的功能。

本专栏文章篇幅有限,除了我分享的这些知识外,还有很多内容值得学习。 我推荐大家阅读 Web API 官方文档 (bit.ly/14cfHIm) 中 Mike Wasson 撰写的 OData 系列优秀文章。 您将学到如何构建所有 CRUD 方法,如何使用 PATCH,甚至是使用注释来限制允许在 OData API 中使用的筛选类型,以及如何处理关系。 请牢记,还有许多其他 Web API 功能适用于 OData API,例如,如何使用授权限制不同人员对不同操作的访问权限。 此外,.NET Web 开发与工具博客 (blogs.msdn.com/webdev) 也提供了大量关于 Web API 中 OData 支持的内容翔实的博文。

Julie Lerman 是 Microsoft MVP、.NET 导师和顾问,居住在佛蒙特州的山区。 您可以在全球的用户组和会议中看到她对数据访问和其他 Microsoft .NET 主题的演示。 她是书籍《Programming Entity Framework》(由 O’Reilly Media 出版)及 Pluralsight.com 上大量在线课程的作者,博客网址为 thedatafarm.com/blog。 有关她的情况,请访问 Twitter 上的 twitter.com/julielerman

衷心感谢以下技术专家对本文的审阅: Jon Galloway (Microsoft) 和 Mike Wasson (Microsoft)
Jon Galloway (Jon.Galloway@microsoft.com) 是 Windows Azure 推广小组的技术推广人员,专注于 ASP.NET MVC 和 ASP.NET Web API。 他广泛活跃于从伊斯坦布尔到班加罗尔再到布宜诺斯艾利斯的会议和国际 Web Camps。 他是《Wrox Professional ASP.NET MVC》图书系列的合著者,也是 Herding Code 播客的搭档主持。
Mike Wasson (mwasson@microsoft.com) 是 Microsoft 的一名程序员兼作家。 多年来,他一直负责撰写 Win32 多媒体 API 的文档。 他目前正在撰写 ASP.NET 的相关内容(以 Web API 为主)。