RESTful 系列第九篇:ASP.NET Web API 中的 OData






4.98/5 (26投票s)
这是 RESTful 系列的最后一篇文章,我将解释如何在 ASP.NET WebAPI 中利用 OData 的功能。我将解释什么是 OData,并创建一个支持 OData 的 RESTful 服务。
目录
引言
这是 RESTful 系列的最后一篇文章,我将解释如何在 ASP.NET WebAPI 中利用 OData 的功能。我将解释什么是 OData,并创建一个支持 OData 的 RESTful 服务。我将尽量使文章简洁,理论少,实践多。
路线图
以下是我为逐步学习WebAPI设置的路线图
- RESTful 第 1 天:使用 Entity Framework、通用仓库模式和工作单元的 Web API 企业级应用程序架构。
- RESTful 第 2 天:使用 Unity 容器和引导程序在 Web API 中使用依赖注入实现控制反转。
- RESTful 系列第三篇:使用 Unity 容器和 Managed Extensibility Framework (MEF) 在 ASP.NET Web API 中通过控制反转和依赖注入解决依赖关系。
- RESTful 第 4 天:使用 MVC 4 Web API 中的属性路由进行自定义 URL 重写/路由。
- RESTful 第 5 天:使用 Action Filter 在 Web API 中实现基本身份验证和基于令牌的自定义授权。
- RESTful 第 6 天:使用 Action Filter、异常过滤器和 NLog 在 Web API 中进行请求日志记录和异常处理/日志记录。
- RESTful 第 7 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第 1 部分)。
- RESTful 第 8 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第 2 部分)。
- RESTful Day #9:在 ASP.NET Web APIs 中扩展 OData 支持。
- RESTful 第 10 天:在 Visual Studio 2010 中创建带有 CRUD 操作的自托管 ASP.NET WebAPI
我将特意使用 Visual Studio 2010 和 .NET Framework 4.0,因为在 .NET Framework 4.0 中,有些实现非常难以找到,但我会通过展示如何实现它们来使其变得容易。
OData
OData 是一个协议,它提供了创建可查询的 REST 服务的灵活性。它提供了一些查询选项,通过这些选项,客户端可以通过 HTTP 从服务器按需获取数据。
以下是来自 ASP.NET 的定义:
"开放数据协议 (OData) 是一个用于 Web 的数据访问协议。OData 通过 CRUD 操作(创建、读取、更新和删除)提供了一种统一的方式来查询和操作数据集。"
更详细的解释来自 此处:
"OData 定义了可用于修改 OData 查询的参数。客户端在请求 URI 的查询字符串中发送这些参数。例如,要对结果进行排序,客户端会使用 $orderby 参数。"
https:///Products?$orderby=Name
"OData 规范将这些参数称为查询选项。您可以为项目中的任何 Web API 控制器启用 OData 查询选项——控制器不需要是 OData 端点。这为您提供了向任何 Web API 应用程序添加筛选和排序等功能的便捷方式。"
假设我们的数据库中的产品表包含超过 50000 个产品,并且我们想根据某些条件(如产品 ID、价格或名称)仅获取前 50 个产品。根据我们当前的服务实现,我必须从服务器数据库获取所有产品,然后在客户端进行筛选。另一种选择是仅在服务器端获取数据,进行筛选,然后将筛选后的数据发送给客户端。在这两种情况下,我都需要编写额外的代码来筛选数据。这时 OData 就派上用场了。OData 允许您创建可查询的服务。如果暴露的服务端点支持 OData,或者支持 OData 查询选项,那么服务实现将考虑 OData 请求并进行相应的处理。因此,如果获取 50 条记录的请求是一个 OData 请求,服务将只从服务器获取 50 条记录。OData 不仅提供筛选功能,还提供搜索、排序、跳过数据、选择数据等功能。我将通过实际实现来解释这个概念。我们将使用我们已经创建的服务,并修改它们以支持 OData 查询选项。
查询选项
以下是 ASP.NET WebAPI 支持的 OData 查询选项:
$orderby
:按特定顺序(例如升序或降序)对获取的记录进行排序。$select
:在结果集中选择列或属性。指定要包含在获取结果中的所有属性。$skip
:用于跳过记录或结果的数量。例如,我想在获取完整的表数据时跳过数据库中的前 100 条记录,那么我可以使用$skip
。$top
:仅获取前 n 条记录。例如,我想从数据库中获取前 10 条记录,那么我的特定服务应该支持 OData 以支持$top
查询选项。$expand
:展开获取实体的相关域实体。$filter
:根据特定条件筛选结果集,类似于 LINQ 的 where 子句。例如,我想获取成绩超过 90% 的 50 名学生的记录,那么我可以使用此查询选项。$inlinecount
:此查询选项主要用于客户端分页。它将从服务器获取的总实体数告知客户端。
设置解决方案
当您从我上一篇文章中获取代码库并在 Visual Studio 中打开它时,您会看到项目结构与下面的图片大致相同。
该解决方案包含 WebAPI 应用程序和相关项目。
步骤 1:点击“工具”->“程序包管理器”->“程序包管理器控制台”。
步骤 2:在程序包管理器控制台中,将默认项目选择为 WebApi,然后运行命令:Install-Package Microsoft.AspNet.WebApi.OData -Version 4.0.0
请注意,由于我们使用的是 VS 2010 和 .NET Framework 4.0,因此我们需要安装兼容的 OData 库。
该命令将下载一些依赖包并将 DLL 引用添加到您的项目引用中。您将在项目引用中找到 OData 引用 DLL。
我们的项目已设置为创建 OData 端点。您可以创建新服务。我将修改我现有的服务来演示 OData 的工作方式。
OData 端点
打开 WebAPI 项目中的 ProductController
类,然后进入 Get()
方法。此方法从数据库获取所有产品记录。代码如下:
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts();
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
return Request.CreateResponse(HttpStatusCode.OK, productEntities);
throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);
}
让我们通过测试客户端运行代码。只需运行应用程序,我们就能得到:
在 URL 末尾添加 /help 并按 Enter 键,您将看到测试客户端。
由于我们的产品控制器是受保护的,因此我们需要从服务中获取经过身份验证的令牌,并使用该令牌来访问产品控制器的所有方法。有关 WebAPI 安全性的详细信息,请参阅 本文。点击 POST authenticate API 方法,然后在测试客户端的 TestAPI 页面中操作。
现在让我们使用凭据发送请求。只需在请求中添加一个标头。标头应如下所示:
Authorization : Basic YWtoaWw6YWtoaWw=
这里 "YWtoaWw6YWtoaWw=" 是我在数据库中 Base64 编码的用户名和密码,即 akhil:akhil。
如果授权成功,您将获得一个令牌。只需保存该令牌以供后续对 Product Controller 的调用。
现在在测试客户端中打开您的产品控制器的“allproducts”端点。
测试端点。
我们收到了包含所有六个产品的响应。
我将使用此控制器方法,使其成为 OData 端点,并对其执行各种查询选项。
在方法上方添加一个名为 [Queryable]
的属性,并在 Request.CreateResponse
中将 productEntities
标记为 productEntities.AsQueryable()
。
[Queryable]
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts().AsQueryable();
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
return Request.CreateResponse(HttpStatusCode.OK, productEntities.AsQueryable());
throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);
}
$top
现在使用 $top
查询选项测试 API。
在这里,我在服务端点后面附加了“?$top=2" (就像我们附加查询字符串一样)。这句话的意思是,我只想从服务中获取前两个产品,结果是:
我们只得到了两个产品。所以您在这里可以看到,使服务端点可查询非常简单,而且我们不必编写新的服务来实现这个结果。让我们尝试一些更多的选项。
$filter
您可以使用此选项对记录执行所有筛选。让我们尝试 $filter
查询选项。假设我们需要获取所有名称为“computer”的产品。您可以使用相同的端点进行筛选,如下所示。
我使用了 $filter=ProductName eq 'computer' 作为查询字符串,这意味着获取产品名称为“computer”的产品。结果是,我们只从产品列表中得到一条记录,因为只有一条记录的产品名称是“computer”。
您可以使用多种方式进行筛选,如下所示。
返回所有名称等于“computer”的产品。
https://:50875/v1/Products/Product/allproducts?$filter=ProductName eq "computer"
返回所有 ID 小于 3 的产品。
https://:50875/v1/Products/Product/allproducts?$filter=ProductId lt 3
逻辑运算符:返回所有 ID >= 3 且 ID <= 5 的产品。
https://:50875/v1/Products/Product/allproducts?$filter=ProductId ge 3 and ProductId le 5
字符串函数:返回名称中包含“IPhone”的所有产品。
https://:50875/v1/Products/Product/allproducts?$filter=substringof('IPhone',ProductName)
筛选选项也可以应用于日期字段。
$orderby
让我们用同一个端点尝试 orderby 查询。
返回所有产品,按产品名称降序排序。
https://:50875/v1/Products/Product/allproducts?$orderby=ProductName desc
输出
[ { "ProductId":6, "ProductName":"Watch" }, { "ProductId":8, "ProductName":"Titan Watch" }, { "ProductId":9, "ProductName":"Laptop Bag" }, { "ProductId":1, "ProductName":"Laptop" }, { "ProductId":11, "ProductName":"IPhone 6S" }, { "ProductId":10, "ProductName":"IPhone 6" }, { "ProductId":4, "ProductName":"IPhone" }, { "ProductId":12, "ProductName":"HP Laptop" }, { "ProductId":2, "ProductName":"computer" }, { "ProductId":5, "ProductName":"Bag" } ]
返回所有产品,按产品名称升序排序。
https://:50875/v1/Products/Product/allproducts?$orderby=ProductName asc
输出
[ { "ProductId": 5, "ProductName": "Bag" }, { "ProductId": 2, "ProductName": "computer" }, { "ProductId": 12, "ProductName": "HP Laptop" }, { "ProductId": 4, "ProductName": "IPhone" }, { "ProductId": 10, "ProductName": "IPhone 6" }, { "ProductId": 11, "ProductName": "IPhone 6S" }, { "ProductId": 1, "ProductName": "Laptop" }, { "ProductId": 9, "ProductName": "Laptop Bag" }, { "ProductId": 8, "ProductName": "Titan Watch" }, { "ProductId": 6, "ProductName": "Watch" } ]
返回所有产品,按产品 ID 降序排序。
https://:50875/v1/Products/Product/allproducts?$orderby=ProductId desc
输出
[ { "ProductId": 12, "ProductName": "HP Laptop" }, { "ProductId": 11, "ProductName": "IPhone 6S" }, { "ProductId": 10, "ProductName": "IPhone 6" }, { "ProductId": 9, "ProductName": "Laptop Bag" }, { "ProductId": 8, "ProductName": "Titan Watch" }, { "ProductId": 6, "ProductName": "Watch" }, { "ProductId": 5, "ProductName": "Bag" }, { "ProductId": 4, "ProductName": "IPhone" }, { "ProductId": 2, "ProductName": "computer" }, { "ProductId": 1, "ProductName": "Laptop" } ]
返回所有产品,按产品 ID 升序排序。
https://:50875/v1/Products/Product/allproducts?$orderby=ProductId asc
输出
[ { "ProductId": 1, "ProductName": "Laptop" }, { "ProductId": 2, "ProductName": "computer" }, { "ProductId": 4, "ProductName": "IPhone" }, { "ProductId": 5, "ProductName": "Bag" }, { "ProductId": 6, "ProductName": "Watch" }, { "ProductId": 8, "ProductName": "Titan Watch" }, { "ProductId": 9, "ProductName": "Laptop Bag" }, { "ProductId": 10, "ProductName": "IPhone 6" }, { "ProductId": 11, "ProductName": "IPhone 6S" }, { "ProductId": 12, "ProductName": "HP Laptop" } ]
$orderby 与 $top
您可以组合使用多个查询选项来获取所需记录。假设我需要获取按 ProductId
升序排序的前五条记录。要实现这一点,我可以编写以下查询。
https://:50875/v1/Products/Product/allproducts?$orderby=ProductId asc&$top=5
输出
[ { "ProductId": 1, "ProductName": "Laptop" }, { "ProductId": 2, "ProductName": "computer" }, { "ProductId": 4, "ProductName": "IPhone" }, { "ProductId": 5, "ProductName": "Bag" }, { "ProductId": 6, "ProductName": "Watch" } ]
上面的输出获取了五条记录,并按 ProductId
排序。
$skip
顾名思义,skip 查询选项用于跳过记录。让我们考虑以下场景。
选择前 5 条,跳过 3 条。
https://:50875/v1/Products/Product/allproducts?$top=5&$skip=3
输出
[ { "ProductId": 5, "ProductName": "Bag" }, { "ProductId": 6, "ProductName": "Watch" }, { "ProductId": 8, "ProductName": "Titan Watch" }, { "ProductId": 9, "ProductName": "Laptop Bag" }, { "ProductId": 10, "ProductName": "IPhone 6" } ]
$skip 与 $orderby
按 ProductName 升序排序并跳过 6 条。
https://:50875/v1/Products/Product/allproducts?$orderby=ProductName asc &$skip=6
输出
[ { "ProductId": 1, "ProductName": "Laptop" }, { "ProductId": 9, "ProductName": "Laptop Bag" }, { "ProductId": 8, "ProductName": "Titan Watch" }, { "ProductId": 6, "ProductName": "Watch" } ]
以下是一些您可以用来创建查询的标准筛选运算符和查询函数,摘自 https://msdn.microsoft.com/en-us/library/gg334767.aspx:
标准过滤运算符
Web API 支持下表中列出的标准 OData 过滤运算符。
运算符 | 描述 | 示例 |
比较运算符 | ||
eq | 等于 | $filter=revenue eq 100000 |
ne | 不等于 | $filter=revenue ne 100000 |
gt | 大于 | $filter=revenue gt 100000 |
ge | 大于等于 | $filter=revenue ge 100000 |
lt | 小于 | $filter=revenue lt 100000 |
le | 小于等于 | $filter=revenue le 100000 |
逻辑运算符 | ||
和 | 逻辑与 | $filter=revenue lt 100000 and revenue gt 2000 |
或 | 逻辑或 | $filter=contains(name,'(sample)') or contains(name,'test') |
not | 逻辑否定 | $filter=not contains(name,'sample') |
分组运算符 | ||
( ) | 优先级分组 | (contains(name,'sample') or contains(name,'test')) and revenue gt 5000 |
标准查询函数
Web API 支持这些标准的 OData 字符串查询函数。
函数 | 示例 |
contains | $filter=contains(name,'(sample)') |
endswith | $filter=endswith(name,'Inc.') |
startswith | $filter=startswith(name,'a') |
分页
您可以创建一个支持分页的端点,这意味着如果您在数据库中有大量数据,并且客户端需要显示每页大约十条记录。因此,最好让服务器本身每请求返回十条记录,这样整个数据负载就不会在网络上传输。这也可以提高服务的性能。
假设您有 10000 条记录在数据库中。您可以启用端点以返回十条记录,并响应初始记录以及要发送的记录数。在这种情况下,当用户导航到下一页时,客户端将每次请求下一组记录,并使用分页选项。要启用分页,只需在 [Queryable]
属性中指定页面大小。例如,[Queryable(PageSize = 10)]
。
因此,我们的方法代码变成:
[Queryable(PageSize = 10)]
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts().AsQueryable();
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
return Request.CreateResponse(HttpStatusCode.OK, productEntities.AsQueryable());
throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);
}
Query Options 约束
您也可以对查询选项设置约束。假设您不希望客户端访问筛选选项或跳过选项,那么您可以在操作级别设置约束以忽略该类型的 API 请求。有四种类型的查询选项约束。
AllowedQueryOptions
示例: [Queryable(AllowedQueryOptions =AllowedQueryOptions.Filter | AllowedQueryOptions.OrderBy)]
上面的查询选项示例表明,API 只允许使用 $filter
和 $orderby
查询。
[Queryable(AllowedQueryOptions =AllowedQueryOptions.Filter | AllowedQueryOptions.OrderBy)]
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts().AsQueryable();
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
return Request.CreateResponse(HttpStatusCode.OK, productEntities.AsQueryable());
throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);
}
所以当我调用带有 $top
查询的端点时:
https://:50875/v1/Products/Product/allproducts?$top=10
我收到了以下响应:
它说:
"Message": "The query specified in the URI is not valid.",
"ExceptionMessage": "Query option 'Top' is not allowed. To allow it, set the 'AllowedQueryOptions' property on QueryableAttribute or QueryValidationSettings."
这意味着它不允许其他类型的查询选项在此 API 端点上工作。
AllowedOrderByProperties
示例: [Queryable(AllowedOrderByProperties = "ProductId")] // 提供列/属性列表
这意味着该端点仅支持按 ProductId
进行排序。您可以指定更多希望启用排序的属性。因此,根据以下代码:
[Queryable(AllowedOrderByProperties = "ProductId")]
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts().AsQueryable();
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
return Request.CreateResponse(HttpStatusCode.OK, productEntities.AsQueryable());
throw new ApiDataException(1000, "Products not found", HttpStatusCode.NotFound);
}
如果我尝试调用 URL: https://:50875/v1/Products/Product/allproducts?$orderby=ProductName desc
它会在响应中显示错误。
它说:
"Message": "The query specified in the URI is not valid.",
"ExceptionMessage": "Order by 'ProductName' is not allowed. To allow it, set the 'AllowedOrderByProperties' property on QueryableAttribute or QueryValidationSettings."
URL: https://:50875/v1/Products/Product/allproducts?$orderby=ProductId desc 将正常工作。
AllowedLogicalOperators
示例: [Queryable(AllowedLogicalOperators = AllowedLogicalOperators.GreaterThan)]
在上面的示例中,语句指出查询只允许大于(例如,“gt”逻辑运算符)运算符,任何其他逻辑运算符(非“gt”)的查询都将返回错误。您可以在应用程序中尝试。
AllowedArithmeticOperators
示例: [Queryable(AllowedArithmeticOperators = AllowedArithmeticOperators.Add)]
在上面的示例中,语句指出 API 调用时只允许使用 Add 算术运算符。您可以在应用程序中尝试。
结论
OData 还有很多内容我无法一次性讲完。目的是让您对使用 OData 可以实现的目标有一个初步的了解。您可以探索更多的选项和属性,并与 REST API 互动。我希望您能够创建一个具有所有必需功能的 WebAPI 应用程序。本系列所有文章附带的代码库可作为创建任何企业级 WebAPI 应用程序的样板。继续探索 REST。祝您编码愉快 :) 从 GitHub 下载完整的源代码。
参考文献
http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options
https://msdn.microsoft.com/en-us/library/azure/gg312156.aspx
其他系列
我的其他系列文章
MVC: https://codeproject.org.cn/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu
OOP: https://codeproject.org.cn/Articles/771455/Diving-in-OOP-Day-Polymorphism-and-Inheritance-Ear