65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (26投票s)

2016年4月1日

CPOL

12分钟阅读

viewsIcon

96645

downloadIcon

5607

这是 RESTful 系列的最后一篇文章,我将解释如何在 ASP.NET WebAPI 中利用 OData 的功能。我将解释什么是 OData,并创建一个支持 OData 的 RESTful 服务。

目录

引言

这是 RESTful 系列的最后一篇文章,我将解释如何在 ASP.NET WebAPI 中利用 OData 的功能。我将解释什么是 OData,并创建一个支持 OData 的 RESTful 服务。我将尽量使文章简洁,理论少,实践多。

路线图

以下是我为逐步学习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 查询选项:

  1. $orderby:按特定顺序(例如升序或降序)对获取的记录进行排序。
  2. $select:在结果集中选择列或属性。指定要包含在获取结果中的所有属性。
  3. $skip:用于跳过记录或结果的数量。例如,我想在获取完整的表数据时跳过数据库中的前 100 条记录,那么我可以使用 $skip
  4. $top:仅获取前 n 条记录。例如,我想从数据库中获取前 10 条记录,那么我的特定服务应该支持 OData 以支持 $top 查询选项。
  5. $expand:展开获取实体的相关域实体。
  6. $filter:根据特定条件筛选结果集,类似于 LINQ 的 where 子句。例如,我想获取成绩超过 90% 的 50 名学生的记录,那么我可以使用此查询选项。
  7. $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

© . All rights reserved.