ASP.NET Web API 2 OData での $select、$expand、$value の使用

作成者: Mike Wasson

ASP.NET 4.x 用 OData Web API 2 での $expand、$select、$value オプションの概要とコード サンプル。 これらのオプションを使うと、クライアントはサーバーから返される表現を制御できます。

  • $expand を使うと、関連するエンティティが応答にインラインで含まれるようになります。
  • $select は、応答に含めるプロパティのサブセットを選択します。
  • $value は、プロパティの生の値を取得します。

スキーマの例

この記事では、Product、Supplier、Category の 3 つのエンティティが定義されている OData サービスを使います。 各製品には、1 つのカテゴリと 1 つのサプライヤーがあります。

Diagram that shows a sample schema for the O Data service, defining a Products, Suppliers, and Categories as its entities.

このエンティティ モデルを定義する C# クラスを次に示します。

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

Product クラスで SupplierCategory に対するナビゲーション プロパティが定義されていることに注意してください。 Category クラスでは、各カテゴリの製品のナビゲーション プロパティが定義されています。

このスキーマの OData エンドポイントを作成するには、ASP.NET Web API での OData エンドポイントの作成に関する記事で説明されているように、Visual Studio 2013 のスキャフォールディングを使います。 Product、Category、Supplier に個別のコントローラーを追加します。

$expand と $select の有効化

Visual Studio 2013 の Web API OData スキャフォールディングは、$expand と $select を自動的にサポートするコントローラーを作成します。 参考までに、コントローラーで $expand と $select をサポートするための要件を次に示します。

コレクションの場合、コントローラーの Get メソッドは IQueryable を返す必要があります。

[Queryable]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

単一エンティティの場合は、SingleResult<T> を返します。T は 0 個または 1 個のエンティティを含む IQueryable です。

[Queryable]
public SingleResult<Category> GetCategory([FromODataUri] int key)
{
    return SingleResult.Create(db.Categories.Where(c => c.ID == key));
}

また、前のコード スニペットで示されているように、[Queryable] 属性で Get メソッドを装飾します。 または、開始時に HttpConfiguration オブジェクトで EnableQuerySupport を呼び出します。 (詳しくは、「OData クエリ オプションの有効化」をご覧ください)。

$expand の使用

OData のエンティティまたはコレクションのクエリを実行するとき、既定の応答には関連エンティティは含まれません。 たとえば、次に示すのは Categories エンティティ セットの既定の応答です。

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {"ID":1,"Name":"Apparel"},
    {"ID":2,"Name":"Toys"}
  ]
}

見るとわかるように、Category エンティティに Products のナビゲーション リンクがあっても、応答に製品は含まれません。 ただし、クライアントでは $expand を使って、カテゴリごとの製品の一覧を取得できます。 $expand オプションは、要求のクエリ文字列に組み込まれます。

GET http://localhost/odata/Categories?$expand=Products

これで、サーバーは、カテゴリごとの製品を、カテゴリとインラインで含めるようになります。 応答ペイロードを次に示します。

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories",
  "value":[
    {
      "Products":[
        {"ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"},
        {"ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"},
        {"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"}
      ],
      "ID":1,
      "Name":"Apparel"
    },
    {
      "Products":[
        {"ID":4,"Name":"Yo-yo","Price":"4.95","CategoryId":2,"SupplierId":"WING"},
        {"ID":5,"Name":"Puzzle","Price":"8.00","CategoryId":2,"SupplierId":"WING"}
      ],
      "ID":2,
      "Name":"Toys"
    }
  ]
}

"value" 配列の各エントリに Products のリストが含まれることに注意してください。

$expand オプションは、展開するナビゲーション プロパティのコンマ区切りリストを受け取ります。 次の要求は、製品に対してカテゴリとサプライヤーの両方を展開します。

GET http://localhost/odata/Products(1)?$expand=Category,Supplier

応答本文を次に示します。

{
  "odata.metadata":"http://localhost/odata/$metadata#Products/@Element",
  "Category": {"ID":1,"Name":"Apparel"},
  "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
  "ID":1,
  "Name":"Hat",
  "Price":"15.00",
  "CategoryId":1,
  "SupplierId":"CTSO"
}

ナビゲーション プロパティの複数のレベルを展開できます。 次の例では、カテゴリに対するすべての製品だけでなく、各製品に対するサプライヤーも含まれます。

GET http://localhost/odata/Categories(1)?$expand=Products/Supplier

応答本文を次に示します。

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories/@Element",
  "Products":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":1,"Name":"Hat","Price":"15.00","CategoryId":1,"SupplierId":"CTSO"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "ID":2,"Name":"Scarf","Price":"12.00","CategoryId":1,"SupplierId":"CTSO"
    },{
      "Supplier":{
        "Key":"FBRK","Name":"Fabrikam, Inc."
      },"ID":3,"Name":"Socks","Price":"5.00","CategoryId":1,"SupplierId":"FBRK"
    }
  ],"ID":1,"Name":"Apparel"
}

既定では、Web API の最大展開深度は 2 に制限されています。 これにより、クライアントは、クエリが非効率的になり、大きな応答が作成される可能性がある、$expand=Orders/OrderDetails/Product/Supplier/Region のような複雑な要求を送信できなくなります。 この既定値をオーバーライドするには、[Queryable] 属性で MaxExpansionDepth プロパティを設定します。

[Queryable(MaxExpansionDepth=4)]
public IQueryable<Category> GetCategories()
{
    return db.Categories;
}

$expand オプションについて詳しくは、OData の公式ドキュメントの「展開システム クエリ オプション ($expand)」を参照してください。

$select の使用

$select オプションは、応答本文に含めるプロパティのサブセットを指定します。 たとえば、各製品の名前と価格のみを取得するには、次のクエリを使います。

GET http://localhost/odata/Products?$select=Price,Name

応答本文を次に示します。

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Price,Name",
  "value":[
    {"Price":"15.00","Name":"Hat"},
    {"Price":"12.00","Name":"Scarf"},
    {"Price":"5.00","Name":"Socks"},
    {"Price":"4.95","Name":"Yo-yo"},
    {"Price":"8.00","Name":"Puzzle"}
  ]
}

同じクエリで $select と $expand を組み合わせることができます。 必ず、展開されたプロパティを $select オプションに含めてください。 たとえば、次の要求は製品名とサプライヤーを取得します。

GET http://localhost/odata/Products?$select=Name,Supplier&$expand=Supplier

応答本文を次に示します。

{
  "odata.metadata":"http://localhost/odata/$metadata#Products&$select=Name,Supplier",
  "value":[
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Hat"
    },
    {
      "Supplier":{"Key":"CTSO","Name":"Contoso, Ltd."},
      "Name":"Scarf"
    },
    {
      "Supplier":{"Key":"FBRK","Name":"Fabrikam, Inc."},
      "Name":"Socks"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Yo-yo"
    },
    {
      "Supplier":{"Key":"WING","Name":"Wingtip Toys"},
      "Name":"Puzzle"
   }
  ]
}

展開されたプロパティ内のプロパティを選ぶこともできます。 次の要求では、Products を展開し、カテゴリ名と製品名を選んでいます。

GET http://localhost/odata/Categories?$expand=Products&$select=Name,Products/Name

応答本文を次に示します。

{
  "odata.metadata":"http://localhost/odata/$metadata#Categories&$select=Name,Products/Name",
  "value":[ 
    {
      "Products":[ {"Name":"Hat"},{"Name":"Scarf"},{"Name":"Socks"} ],
      "Name":"Apparel"
    },
    {
      "Products":[ {"Name":"Yo-yo"},{"Name":"Puzzle"} ],
      "Name":"Toys"
    }
  ]
}

$select オプションについて詳しくは、OData の公式ドキュメントの「選択システム クエリ オプション ($select)」を参照してください。

エンティティの個々のプロパティの取得 ($value)

OData クライアントでエンティティから個々のプロパティを取得するには、2 つの方法があります。 クライアントは、OData 形式で値を取得するか、プロパティの生の値を取得することができます。

次の要求は、OData 形式でプロパティを取得します。

GET http://localhost/odata/Products(1)/Name

次に示すのは、JSON 形式の応答の例です。

HTTP/1.1 200 OK
Content-Type: application/json; odata=minimalmetadata; streaming=true; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 90

{
  "odata.metadata":"http://localhost:14239/odata/$metadata#Edm.String",
  "value":"Hat"
}

プロパティの生の値を取得するには、URI に $value を追加します。

GET http://localhost/odata/Products(1)/Name/$value

応答を次に示します。 コンテンツ タイプが JSON ではなく "text/plain" であることに注意してください。

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Hat

OData コントローラーでこれらのクエリをサポートするには、GetProperty という名前のメソッドを追加します。Property はプロパティの名前です。 たとえば、Name プロパティを取得するメソッドは GetName という名前になります。 このメソッドは、そのプロパティの値を返します。

public async Task<IHttpActionResult> GetName(int key)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product.Name);
}