支援 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; }
}

但我們不想讓用戶端將 物件 POST ProductRating 到 「Ratings」集合。 直覺上,評等與 Products 集合相關聯,而用戶端應該只需要張貼評等值。

因此,我們定義用戶端可以在 Product 上叫用的動作,而不是使用一般 CRUD 作業。 在 OData 術語中,動作 會系結 至 Product 實體。

動作會對伺服器造成副作用。 因此,系統會使用 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 實體。 若要實作動作,請將名為 RateProduct 的方法新增至 Products 控制器:

[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 中的動作名稱。 方法有兩個參數:

  • 索引鍵:要評分的產品金鑰。
  • parameters:動作參數值的字典。

如果您使用預設路由慣例,金鑰參數必須命名為 「key」。 也請務必包含 [FromOdataUri] 屬性,如下所示。 當 Web API 從要求 URI 剖析金鑰時,此屬性會告訴 Web API 使用 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

我將討論本主題稍後) 也稱為 暫時 性動作 (不一定可系結的動作。

叫用動作

現在讓我們看看用戶端如何叫用此動作。 假設用戶端想要為識別碼 = 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

具有集合參數的動作

動作可以有採用值集合的參數。 在 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 回 。

其他資源