在 ASP.NET Web API 2 中支援 OData 動作
演講者:Mike Wasson
在 OData 中,動作是新增伺服器端行為的一種方法,這些行為不容易定義為實體上的 CRUD 操作。 動作的用途包括:
- 實作複雜的交易。
- 同時操縱多個實體。
- 僅允許更新實體的某些屬性。
- 向伺服器傳送未在實體中定義的資訊。
教學課程中使用的軟體版本
- Web API 2
- OData 版本 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; }
}
但我們不希望用戶端將 ProductRating
物件發佈到「Ratings」集合中。 直觀上,評分與產品集合相關聯,客戶只需要發布評分值。
因此,我們不使用普通的 CRUD 動作,而是定義用戶端可以在產品上叫用的動作。 在 OData 術語中,動作會繫結到產品實體。
動作會對伺服器產生副作用。 因此,它們是使用 HTTP POST 請求來叫用的。 動作可以具有參數和傳回類型,這些在服務中繼資料中進行了描述。 用戶端在請求本文中傳送參數,伺服器在回應本文中傳送傳回值。 若要叫用「評價產品」動作,用戶端需要將 POST 傳送到如下所示的 URI:
http://localhost/odata/Products(1)/RateProduct
POST 請求中的資料只是產品評分:
{"Rating":2}
在實體資料模型中宣告動作
在您的 Web API 設定中,將動作新增至實體資料模型 (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());
}
}
此程式碼將「RateProduct」定義為可以對 Product 實體執行的動作。 它還宣告該動作採用名為「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 中的動作名稱相符。 此方法有兩個參數:
- key:產品評分的按鍵。
- parameters:動作參數值的字典。
如果您使用預設路由慣例,則 key 參數必須命名為「key」。 也請務必加入 [FromOdataUri] 屬性,如下所示。 此屬性告訴 Web API 在解析請求 URI 中的金鑰時,使用 OData 語法規則。
使用參數字典取得動作參數:
if (!ModelState.IsValid)
{
return BadRequest();
}
int rating = (int)parameters["Rating"];
如果用戶端以正確的格式傳送動作參數,則 ModelState.IsValid 的值為 True。 在這種情況下,您可以使用 ODataActionParameters 字典來取得參數值。 在此範例中,RateProduct
動作採用名為「Rating」的單一參數。
動作中繼資料
若要查看服務中繼資料,請將 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 元素宣告動作。 大多數欄位都一目了然,但有兩個欄位值得注意:
- IsBindable 代表至少在某些時候可以在目標實體上叫用該動作。
- IsAlwaysBindable 代表始終可以在目標實體上叫用該動作。
不同之處在於,某些動作始終可供用戶端使用,但其他動作可能取決於實體的狀態。 例如,假設您定義「購買」動作。 您只能購買有庫存的商品。 如果該商品缺貨,則用戶端無法叫用該動作。
當您定義 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
}
將動作繫結到實體集
在前面的範例中,動作繫結到單一實體:用戶端對單一產品進行評分。 您也可以將動作繫結到實體集合。 只需進行以下更改:
在 EDM 中,將動作新增至實體的 Collection 屬性。
var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");
在控制器方法中,省略 key 參數。
[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
// ....
}
現在用戶端叫用 Products 實體集上的動作:
http://localhost/odata/Products/RateAllProducts
帶有 Collection 參數的動作
動作可以具有採用值集合的參數。 在 EDM 中,使用 CollectionParameter<T> 宣告參數。
rateAllProducts.CollectionParameter<int>("Ratings");
這宣告了一個名為「Ratings」的參數,該參數採用 int 值的集合。 在控制器方法中,您仍然從 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」範例中,使用者永遠可以對產品進行評分,因此該動作永遠可用。 但某些動作取決於實體的狀態。 例如,在視訊租賃服務中,「結帳」動作並非永遠可用。 (這取決於該影片的副本是否可用。) 這種類型的動作稱為暫態動作。
在服務中繼資料中,瞬態動作的 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 設定此函式。 您可以將該函式編寫為 lambda 運算式:
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);
如果該動作可用,則 lambda 運算式會傳回該動作的連結。 OData 序列化程序在序列化實體時包含此連結。 當該動作不可用時,函式會傳回 null
。