ASP.NET Web API 2 中支援 OData 查詢選項

作者:Mike Wasson

此概觀與程式碼範例示範 ASP.NET 4.x ASP.NET Web API 2 中支援的 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 告知伺服器在回應中包含相符實體的總計數。 (適用于伺服器端分頁.)
$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
邏輯運算子:傳回所有價格 = 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>;
}