支持 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>;
}