支持 ASP.NET Web API 2 中的 OData 查询选项
作者:Mike Wasson
此包含代码示例的概述演示了 ASP.NET Web API 2 中支持 ASP.NET 4.x 的 OData 查询选项。
OData 定义可用于修改 OData 查询的参数。 客户端在请求 URI 的查询字符串中发送这些参数。 例如,若要对结果进行排序,客户端使用 $orderby 参数:
http://localhost/Products?$orderby=Name
OData 规范调用这些参数 查询选项。 可以为项目中的任何 Web API 控制器启用 OData 查询选项 - 控制器不需要是 OData 终结点。 这为你提供了一种向任何 Web API 应用程序添加筛选和排序等功能的便捷方法。
启用查询选项之前,请阅读主题 OData 安全指南。
启用 OData 查询选项
Web API 支持以下 OData 查询选项:
选项 | 说明 |
---|---|
$expand | 内联展开相关实体。 |
$filter | 根据布尔条件筛选结果。 |
$inlinecount | 告知服务器在响应中包含匹配实体的总计数。 (适用于服务器端 paging.) |
$orderby | 对结果进行排序。 |
$select | 选择要包含在响应中的属性。 |
$skip | 跳过前 n 个结果。 |
$top | 仅返回结果的前 n 个。 |
若要使用 OData 查询选项,必须显式启用它们。 可以针对整个应用程序全局启用它们,也可以为特定控制器或特定操作启用它们。
若要全局启用 OData 查询选项,请在启动时对 HttpConfiguration 类调用 EnableQuerySupport:
public static void Register(HttpConfiguration config)
{
// ...
config.EnableQuerySupport();
// ...
}
EnableQuerySupport 方法为返回 IQueryable 类型的任何控制器操作全局启用查询选项。 如果不希望为整个应用程序启用查询选项,可以通过将 [Queryable] 属性添加到操作方法,为特定的控制器操作启用查询选项。
public class ProductsController : ApiController
{
[Queryable]
IQueryable<Product> Get() {}
}
查询示例
本部分显示可以使用 OData 查询选项的查询类型。 有关查询选项的具体详细信息,请参阅 www.odata.org 中的 OData 文档。
有关$expand和$select的信息,请参阅在 ASP.NET Web API OData 中使用$select、$expand和$value。
客户端驱动的分页
对于大型实体集,客户端可能需要限制结果数。 例如,客户端可能一次显示 10 个条目,并使用“下一页”链接获取下一页的结果。 为此,客户端使用 $top 和 $skip 选项。
http://localhost/Products?$top=10&$skip=20
$top选项提供要返回的最大条目数,$skip选项提供要跳过的条目数。 上一个示例提取条目 21 到 30。
筛选
$filter选项允许客户端通过应用布尔表达式来筛选结果。 筛选器表达式非常强大;它们包括逻辑运算符和算术运算符、字符串函数和日期函数。
返回类别等于“Toys”的所有产品。 | http://localhost/Products?$filter=Category eq 'Toys' |
---|---|
返回价格低于 10 的所有产品。 | http://localhost/Products?$filter=Price lt 10 |
逻辑运算符:返回 price = 5 且 price ><= 15 的所有产品。 | http://localhost/Products?$filter=Price ge 5 和 Price le 15 |
字符串函数:返回名称中包含“zz”的所有产品。 | http://localhost/Products?$filter=substringof('zz',Name) |
日期函数:返回 2005 年之后具有 ReleaseDate 的所有产品。 | http://localhost/Products?$filter=year(ReleaseDate) gt 2005 |
排序
若要对结果进行排序,请使用$orderby筛选器。
按价格排序。 | http://localhost/Products?$orderby=Price |
---|---|
按价格按降序排序 (最高到最低) 。 | http://localhost/Products?$orderby=Price desc |
按类别排序,然后在类别中按价格降序排序。 | http://localhost/odata/Products?$orderby=Category,Price desc |
Server-Driven分页
如果数据库包含数百万条记录,则不希望在一个有效负载中发送所有记录。 为了防止这种情况,服务器可以限制在单个响应中发送的条目数。 若要启用服务器分页,请在 Queryable 属性中设置 PageSize 属性。 该值是要返回的最大条目数。
[Queryable(PageSize=10)]
public IQueryable<Product> Get()
{
return products.AsQueryable();
}
如果控制器返回 OData 格式,则响应正文将包含指向下一页数据的链接:
{
"odata.metadata":"http://localhost/$metadata#Products",
"value":[
{ "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
{ "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
// Others not shown
],
"odata.nextLink":"http://localhost/Products?$skip=10"
}
客户端可以使用此链接提取下一页。 若要了解结果集中的条目总数,客户端可以使用值“allpages”设置$inlinecount查询选项。
http://localhost/Products?$inlinecount=allpages
值“allpages”告知服务器在响应中包含总计数:
{
"odata.metadata":"http://localhost/$metadata#Products",
"odata.count":"50",
"value":[
{ "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
{ "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
// Others not shown
]
}
注意
下一页链接和内联计数都需要 OData 格式。 原因是 OData 在响应正文中定义特殊字段来保存链接和计数。
对于非 OData 格式,仍可以通过将查询结果包装在 PageResult<T> 对象中来支持下一页链接和内联计数。 但是,它需要更多的代码。 以下是示例:
public PageResult<Product> Get(ODataQueryOptions<Product> options)
{
ODataQuerySettings settings = new ODataQuerySettings()
{
PageSize = 5
};
IQueryable results = options.ApplyTo(_products.AsQueryable(), settings);
return new PageResult<Product>(
results as IEnumerable<Product>,
Request.GetNextPageLink(),
Request.GetInlineCount());
}
下面是 JSON 响应示例:
{
"Items": [
{ "ID":1,"Name":"Hat","Price":"15","Category":"Apparel" },
{ "ID":2,"Name":"Socks","Price":"5","Category":"Apparel" },
// Others not shown
],
"NextPageLink": "http://localhost/api/values?$inlinecount=allpages&$skip=10",
"Count": 50
}
限制查询选项
查询选项使客户端可以大量控制服务器上运行的查询。 在某些情况下,出于安全或性能原因,可能需要限制可用选项。 [Queryable] 属性对此具有一些内置属性。 下面是一些示例。
仅允许$skip和$top,以支持分页和其他任何内容:
[Queryable(AllowedQueryOptions=
AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
仅允许按某些属性排序,以防止对数据库中未编制索引的属性进行排序:
[Queryable(AllowedOrderByProperties="Id")] // comma-separated list of properties
允许“eq”逻辑函数,但不允许其他逻辑函数:
[Queryable(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
不允许任何算术运算符:
[Queryable(AllowedArithmeticOperators=AllowedArithmeticOperators.None)]
可以通过构造 QueryableAttribute 实例并将其传递给 EnableQuerySupport 函数来全局限制选项:
var queryAttribute = new QueryableAttribute()
{
AllowedQueryOptions = AllowedQueryOptions.Top | AllowedQueryOptions.Skip,
MaxTop = 100
};
config.EnableQuerySupport(queryAttribute);
直接调用查询选项
可以直接在控制器中调用查询选项,而不是使用 [Queryable] 属性。 为此,请将 ODataQueryOptions 参数添加到控制器方法。 在这种情况下,不需要 [Queryable] 属性。
public IQueryable<Product> Get(ODataQueryOptions opts)
{
var settings = new ODataValidationSettings()
{
// Initialize settings as needed.
AllowedFunctions = AllowedFunctions.AllMathFunctions
};
opts.Validate(settings);
IQueryable results = opts.ApplyTo(products.AsQueryable());
return results as IQueryable<Product>;
}
Web API 从 URI 查询字符串填充 ODataQueryOptions 。 若要应用查询,请将 IQueryable 传递给 ApplyTo 方法。 方法返回另一个 IQueryable。
对于高级方案,如果没有 IQueryable 查询提供程序,可以检查 ODataQueryOptions 并将查询选项转换为另一种形式。 (例如,请参阅 RaghuRam Nadiminti 的博客文章 将 OData 查询转换为 HQL)
查询验证
[Queryable] 属性在执行查询之前对其进行验证。 验证步骤在 QueryableAttribute.ValidateQuery 方法中执行。 还可以自定义验证过程。
另请参阅 OData 安全指南。
首先,重写 在 Web.Http.OData.Query.Validators 命名空间中定义的验证程序类之一。 例如,以下验证程序类禁用$orderby选项的“desc”选项。
public class MyOrderByValidator : OrderByQueryValidator
{
// Disallow the 'desc' parameter for $orderby option.
public override void Validate(OrderByQueryOption orderByOption,
ODataValidationSettings validationSettings)
{
if (orderByOption.OrderByNodes.Any(
node => node.Direction == OrderByDirection.Descending))
{
throw new ODataException("The 'desc' option is not supported.");
}
base.Validate(orderByOption, validationSettings);
}
}
子类 [Queryable] 属性以替代 ValidateQuery 方法。
public class MyQueryableAttribute : QueryableAttribute
{
public override void ValidateQuery(HttpRequestMessage request,
ODataQueryOptions queryOptions)
{
if (queryOptions.OrderBy != null)
{
queryOptions.OrderBy.Validator = new MyOrderByValidator();
}
base.ValidateQuery(request, queryOptions);
}
}
然后全局或按控制器设置自定义属性:
// Globally:
config.EnableQuerySupport(new MyQueryableAttribute());
// Per controller:
public class ValuesController : ApiController
{
[MyQueryable]
public IQueryable<Product> Get()
{
return products.AsQueryable();
}
}
如果直接使用 ODataQueryOptions ,请在选项上设置验证程序:
public IQueryable<Product> Get(ODataQueryOptions opts)
{
if (opts.OrderBy != null)
{
opts.OrderBy.Validator = new MyOrderByValidator();
}
var settings = new ODataValidationSettings()
{
// Initialize settings as needed.
AllowedFunctions = AllowedFunctions.AllMathFunctions
};
// Validate
opts.Validate(settings);
IQueryable results = opts.ApplyTo(products.AsQueryable());
return results as IQueryable<Product>;
}