ASP.NET Web API 2 での OData アクションのサポート
作成者: Mike Wasson
OData では、アクションは、エンティティに対する CRUD 操作として簡単には定義されないサーバー側の動作を追加する方法です。 アクションには、次のような用途があります。
- 複雑なトランザクションの実装。
- 一度に複数のエンティティを操作。
- エンティティの特定のプロパティに対してのみ更新を許可する。
- エンティティで定義されていないサーバーに情報を送信する。
チュートリアルで使用するソフトウェアのバージョン
- Web API 2
- OData Version 3
- Entity Framework 6
例: 製品の評価
この例では、ユーザーに製品を評価させ、各製品の平均評価を公開します。 データベースには、製品にキーを設定した評価の一覧が格納されます。
Entity Framework の評価を表示するために使用できるモデルを次に示します。
public class ProductRating
{
public int ID { get; set; }
[ForeignKey("Product")]
public int ProductID { get; set; }
public virtual Product Product { get; set; } // Navigation property
public int Rating { get; set; }
}
ただし、クライアントが "Ratings" コレクションに ProductRating
オブジェクトを POSTする必要はありません。 直感的に、評価は Products コレクションに関連付けられ、クライアントは評価値を投稿するだけで済みます。
そのため、通常の CRUD 操作を使用する代わりに、クライアントが製品に対して呼び出すことができるアクションを定義します。 OData の用語では、アクションは Product エンティティにバインドされます。
アクションはサーバーに副作用を与えます。 このため、HTTP POST 要求を使用して呼び出されます。 アクションにはパラメーターと戻り値の型があり、サービスのメタデータに記述されています。 クライアントは要求本文でパラメーターを送信し、サーバーは応答本文で戻り値を送信します。 "Rate Product" アクションを呼び出すために、クライアントは次のような URI に POST を送信します。
http://localhost/odata/Products(1)/RateProduct
POST 要求のデータは、単に製品評価です。
{"Rating":2}
Entity Data Model でアクションを宣言する
Web API 構成で、アクションを Entity Data Model (EDM) に追加します。
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
builder.EntitySet<Supplier>("Suppliers");
builder.EntitySet<ProductRating>("Ratings");
// New code: Add an action to the EDM, and define the parameter and return type.
ActionConfiguration rateProduct = builder.Entity<Product>().Action("RateProduct");
rateProduct.Parameter<int>("Rating");
rateProduct.Returns<double>();
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
}
このコードでは、Product エンティティに対して実行できるアクションとして "RateProduct" を定義しています。 また、アクションが "Rating" という名前の int パラメーターを受け取り、int 値を返することも宣言します。
コントローラーにアクションを追加する
"RateProduct" アクションは Product エンティティにバインドされます。 アクションを実装するには、Products コントローラーに RateProduct
という名前のメソッドを追加します。
[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
int rating = (int)parameters["Rating"];
Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
}
product.Ratings.Add(new ProductRating() { Rating = rating });
db.SaveChanges();
double average = product.Ratings.Average(x => x.Rating);
return Ok(average);
}
メソッド名が EDM 内のアクションの名前と一致していることにご注意ください。 このメソッドには 2 つのパラメーターがあります。
- key: 製品を評価するためのキー。
- parameters: アクション パラメーター値のディクショナリ。
既定のルーティング規則を使用している場合は、キー パラメーターに "key" という名前を付ける必要があります。 次に示すように、[FromOdataUri] 属性を含めることも重要です。 この属性は、要求 URI からキーを解析するときに OData 構文規則を使用するように Web API に指示します。
アクション パラメーターを取得するには、パラメーター ディクショナリを使用します。
if (!ModelState.IsValid)
{
return BadRequest();
}
int rating = (int)parameters["Rating"];
クライアントがアクション パラメーターを正しい形式で送信する場合、ModelState.IsValid の値は true になります。 その場合は、ODataActionParameters ディクショナリを使用してパラメーター値を取得できます。 この例では、RateProduct
アクションは "Rating" という名前の 1 つのパラメーターを受け取ります。
アクション メタデータ
サービス メタデータを表示するには、GET 要求を /odata/$metadata に送信します。 RateProduct
アクションを宣言するメタデータの部分を次に示します。
<FunctionImport Name="RateProduct" m:IsAlwaysBindable="true" IsBindable="true" ReturnType="Edm.Double">
<Parameter Name="bindingParameter" Type="ProductService.Models.Product"/>
<Parameter Name="Rating" Nullable="false" Type="Edm.Int32"/>
</FunctionImport>
FunctionImport 要素は、アクションを宣言します。 ほとんどのフィールドは自明ですが、次の 2 つのフィールドには注意してください。
- IsBindable は、少なくとも一部の時点で、ターゲット エンティティ上でアクションを呼び出すことができることを意味します。
- IsAlwaysBindable は、ターゲット エンティティでアクションを常に呼び出すことができることを意味します。
この 2 つのフィールドには、一部のアクションは常にクライアントで使用できますが、他のアクションはエンティティの状態に依存する場合があるという違いがあります。 たとえば、"購入" アクションを定義するとします。 在庫のある製品のみご購入いただけます。 製品が在庫切れの場合、クライアントは購入アクションを呼び出すことができません。
EDM を定義すると、Action メソッドによって常にバインド可能なアクションが作成されます。
builder.Entity<Product>().Action("RateProduct"); // Always bindable
常時バインド可能ではないアクション (一時的なアクションとも呼ばれます) については、このトピックの後半で説明します。
アクションの呼び出し
次に、クライアントがこのアクションを呼び出す方法を見てみましょう。 クライアントが ID = 4 の製品に 2 の評価を与えたいとします。 要求本文に JSON 形式を使用した要求メッセージの例を次に示します。
POST http://localhost/odata/Products(4)/RateProduct HTTP/1.1
Content-Type: application/json
Content-Length: 12
{"Rating":2}
応答メッセージを次に示します。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 3.0
Date: Tue, 22 Oct 2013 19:04:00 GMT
Content-Length: 89
{
"odata.metadata":"http://localhost:21900/odata/$metadata#Edm.Double","value":2.75
}
エンティティ セットへのアクションのバインド
前の例では、アクションは 1 つのエンティティにバインドされていて、クライアントは 1 つの製品を評価します。 アクションをエンティティのコレクションにバインドすることもできます。 次の変更を加えます。
EDM で、エンティティの Collection プロパティにアクションを追加します。
var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");
コントローラー メソッドで、キー パラメーターを省略します。
[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
// ....
}
次に、クライアントは Products エンティティ セットに対してアクションを呼び出します。
http://localhost/odata/Products/RateAllProducts
コレクション パラメーターを使用したアクション
アクションには、値のコレクションを受け取るパラメーターを指定できます。 EDM で、CollectionParameter<T> を使用してパラメーターを宣言します。
rateAllProducts.CollectionParameter<int>("Ratings");
これにより、int 値のコレクションを受け取る "Ratings" という名前のパラメーターが宣言されます。 コントローラー メソッドでは、引き続き ODataActionParameters オブジェクトからパラメーター値を取得しますが、その値は ICollection<int> 値になります。
[HttpPost]
public void RateAllProducts(ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
var ratings = parameters["Ratings"] as ICollection<int>;
// ...
}
一時的なアクション
"RateProduct" の例では、ユーザーは常に製品を評価できるため、アクションは常に使用可能です。 ただし、一部のアクションはエンティティの状態に依存します。 たとえば、ビデオ レンタル サービスでは、"CheckOut" アクションは常に使用できるわけではありません。 (そのビデオのコピーが使用可能かどうかによって異なります。)この種類のアクションは、一時的なアクションと呼ばれます。
サービス メタデータでは、一時的なアクションの IsAlwaysBindable は false になります。 実際にはこの値が既定値のため、メタデータは次のようになります。
<FunctionImport Name="CheckOut" IsBindable="true">
<Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>
これが重要な理由は次のとおりです。アクションが一時的なものである場合、サーバーはそのアクションがいつ使用可能かをクライアントに通知する必要があります。 これは、エンティティにアクションへのリンクを含めることで行われます。 Movie エンティティの例を次に示します。
{
"odata.metadata":"http://localhost:17916/odata/$metadata#Movies/@Element",
"#CheckOut":{ "target":"http://localhost:17916/odata/Movies(1)/CheckOut" },
"ID":1,"Title":"Sudden Danger 3","Year":2012,"Genre":"Action"
}
"#CheckOut" プロパティには、CheckOut アクションへのリンクが含まれています。 アクションが使用できない場合、サーバーはリンクを省略します。
EDM で一時的なアクションを宣言するには、TransientAction メソッドを呼び出します。
var checkoutAction = builder.Entity<Movie>().TransientAction("CheckOut");
また、指定したエンティティのアクション リンクを返す関数を指定する必要があります。 HasActionLink を呼び出して、この関数を設定します。 この関数はラムダ式として記述できます。
checkoutAction.HasActionLink(ctx =>
{
var movie = ctx.EntityInstance as Movie;
if (movie.IsAvailable) {
return new Uri(ctx.Url.ODataLink(
new EntitySetPathSegment(ctx.EntitySet),
new KeyValuePathSegment(movie.ID.ToString()),
new ActionPathSegment(checkoutAction.Name)));
}
else
{
return null;
}
}, followsConventions: true);
アクションが使用可能な場合、ラムダ式はアクションへのリンクを返します。 OData シリアライザーは、エンティティをシリアル化するときにこのリンクを含めます。 アクションが使用できない場合、関数は null
を返します。