次の方法で共有


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 をフェッチします。

Filtering

$filter オプションを使用すると、クライアントはブール式を適用して結果をフィルター処理できます。 フィルター式は非常に強力です。これには、論理演算子と算術演算子、文字列関数、日付関数が含まれます。

カテゴリが "Toys" と等しいすべての製品を返します。 http://localhost/Products?$filter=Category eq 'Toys'
価格が 10 未満のすべての製品を返します。 http://localhost/Products?$filter=Price lt 10
論理演算子: 価格 >= 5 かつ価格 <= 15 のすべての製品を返します。 http://localhost/Products?$filter=Price ge 5 and Price le 15
文字列関数: 名前に "zz" が含まれるすべての製品を返します。 http://localhost/Products?$filter=substringof('zz',Name)
日付関数: ReleaseDate が 2005 より後のすべての製品を返します。 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

サーバー駆動型ページング

データベースに何百万ものレコードが含まれている場合は、それらすべてを 1 つのペイロードで送信する必要はありません。 これを防ぐために、サーバーは 1 つの応答で送信するエントリの数を制限できます。 サーバーのページングを有効にするには、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
  ]
}

Note

次ページ リンクとインライン カウントの両方に 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 を設定します。 クエリを適用するには、 ApplyTo メソッドに IQueryable を渡します。 このメソッドは、別の IQueryable を返します。

高度なシナリオでは、IQueryable クエリ プロバイダーがない場合は、ODataQueryOptions を調べて、クエリ オプションを別のフォームに変換できます。 (たとえば、RaghuRam Nadimint のブログ記事「OData クエリを HQL に変換する」を参照してください)

クエリの検証

[Queryable] 属性は、クエリを実行する前にクエリを検証します。 検証手順は、QueryableAttribute.ValidateQuery メソッドで実行されます。 検証プロセスをカスタマイズすることもできます。

OData セキュリティ ガイダンス」も参照してください。

まず、Web.Http.OData.Query.Validators 名前空間で定義されている検証コントロール クラスの 1 つをオーバーライドします。 たとえば、次の検証コントロール クラスは、$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>;
}