共用方式為


在 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

其他資源