共用方式為


使用 Web API 2 支援 OData v3 中的實體關係

演講者:Mike Wasson

下載已完成的專案

大多數資料集定義實體之間的關係:客戶有訂單;書籍有作者;產品有供應商。 使用 OData,用戶端可以導覽實體關係。 給定一個產品,您可以找到供應商。 您也可以建立或刪除關係。 例如,您可以設定產品的供應商。

本教學課程示範如何在 ASP.NET Web API 中支援這些操作。 本教學課程是基於使用 Web API 2 建立 OData v3 端點教學課程

教學課程中使用的軟體版本

  • Web API 2
  • OData 版本 3
  • Entity Framework 6

新增供應商實體

首先,我們需要在 OData feed 中新增新的實體類型。 我們將新增一個 Supplier 類別。

using System.ComponentModel.DataAnnotations;

namespace ProductService.Models
{
    public class Supplier
    {
        [Key]
        public string Key { get; set; }
        public string Name { get; set; }
    }
}

此類使用字串作為實體鍵。 實際上,這可能不如使用整數金鑰常見。 但值得一看的是 OData 如何處理整數之外的其他金鑰類型。

接下來,我們將透過向 Product 類別新增 Supplier 屬性來建立關係:

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

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

ProductServiceContext 類別中新增一個新的 DbSet,以便 Entity Framework 將在資料庫中包含 Supplier 資料表。

public class ProductServiceContext : DbContext
{
    public ProductServiceContext() : base("name=ProductServiceContext")
    {
    }

    public System.Data.Entity.DbSet<ProductService.Models.Product> Products { get; set; }
    // New code:
    public System.Data.Entity.DbSet<ProductService.Models.Supplier> Suppliers { get; set; }
}

在 WebApiConfig.cs 中,將「供應商」實體新增至 EDM 模型:

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
// New code:
builder.EntitySet<Supplier>("Suppliers");

若要取得產品的供應商,用戶端傳送 GET 請求:

GET /Products(1)/Supplier

這裡的「供應商」是 Product 類型的導覽屬性。 在本例中,Supplier 指的是單一項目,但導覽屬性也可以傳回集合 (一對多或多對多關係)。

若要支援此請求,請將以下方法新增至 ProductsController 類別:

// GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
    Product product = _context.Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return product.Supplier;
}

金鑰參數是產品的金鑰。 此方法傳回相關實體-在本例中是一個 Supplier 執行個體。 方法名稱和參數名稱都很重要。 一般來說,如果導覽屬性名為「X」,則需要新增名為「GetX」的方法。 此方法必須採用名為「金鑰」的參數,該參數與上層金鑰的資料類型相符。

金鑰參數中包含 [FromOdataUri] 屬性也很重要。 此屬性告訴 Web API 在解析請求 URI 中的金鑰時,使用 OData 語法規則。

OData 支援建立或刪除兩個實體之間的關係。 在 OData 術語中,關係是「連結」。每個連結都有一個格式為entity/$links/entity 的URI。 例如,從產品到供應商的連結如下所示:

/Products(1)/$links/Supplier

要建立新連結,用戶端向連結 URI 傳送 POST 請求。 請求的本文是目標實體的 URI。 例如,假設有一個供應商的金鑰為「CTSO」。 要建立從「Product(1)」到「Supplier('CTSO')」的連結,用戶端傳送如下請求:

POST http://localhost/odata/Products(1)/$links/Supplier
Content-Type: application/json
Content-Length: 50

{"url":"http://localhost/odata/Suppliers('CTSO')"}

要刪除連結,用戶端向連結 URI 傳送 DELETE 請求。

建立連結

要使用戶端能夠建立產品供應商連結,請將以下程式碼新增到 ProductsController 類別中:

[AcceptVerbs("POST", "PUT")]
public async Task<IHttpActionResult> CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
            
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
            
    switch (navigationProperty)
    {
        case "Supplier":
            string supplierKey = GetKeyFromLinkUri<string>(link);
            Supplier supplier = await db.Suppliers.FindAsync(supplierKey);
            if (supplier == null)
            {
                return NotFound();
            }
            product.Supplier = supplier;
            await db.SaveChangesAsync();
            return StatusCode(HttpStatusCode.NoContent);

        default:
            return NotFound();
    }
}

此方法需要三個參數:

  • 金鑰:上層實體 (產品) 的金鑰
  • navigationProperty:導覽屬性的名稱。 在此範例中,唯一有效的導覽屬性是「Supplier」。
  • 連結:相關實體的 OData URI。 該值取自請求本文。 例如,連結 URI 可能是「http://localhost/odata/Suppliers('CTSO'),表示 ID =‘CTSO’的供應商。

該方法使用連結來查找供應商。 如果找到匹配的供應商,該方法將設定 Product.Supplier 屬性並將結果儲存到資料庫中。

最困難的部分是解析連結 URI。 基本上,您需要模擬向該 URI 傳送 GET 請求的結果。 以下輔助方法展示如何執行此操作。 此方法呼叫 Web API 路由程序並傳回表示解析的 OData 路徑的 ODataPath 執行個體。 對於連結 URI,其中一個段落應該是實體鍵。 (如果不是,則用戶端傳送了錯誤的 URI。)

// Helper method to extract the key from an OData link URI.
private TKey GetKeyFromLinkUri<TKey>(Uri link)
{
    TKey key = default(TKey);

    // Get the route that was used for this request.
    IHttpRoute route = Request.GetRouteData().Route;

    // Create an equivalent self-hosted route. 
    IHttpRoute newRoute = new HttpRoute(route.RouteTemplate, 
        new HttpRouteValueDictionary(route.Defaults), 
        new HttpRouteValueDictionary(route.Constraints),
        new HttpRouteValueDictionary(route.DataTokens), route.Handler);

    // Create a fake GET request for the link URI.
    var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link);

    // Send this request through the routing process.
    var routeData = newRoute.GetRouteData(
        Request.GetConfiguration().VirtualPathRoot, tmpRequest);

    // If the GET request matches the route, use the path segments to find the key.
    if (routeData != null)
    {
        ODataPath path = tmpRequest.GetODataPath();
        var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
        if (segment != null)
        {
            // Convert the segment into the key type.
            key = (TKey)ODataUriUtils.ConvertFromUriLiteral(
                segment.Value, ODataVersion.V3);
        }
    }
    return key;
}

刪除連結

要刪除連結,請將以下程式碼新增至 ProductsController 類別:

public async Task<IHttpActionResult> DeleteLink([FromODataUri] int key, string navigationProperty)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    switch (navigationProperty)
    {
        case "Supplier":
            product.Supplier = null;
            await db.SaveChangesAsync();
            return StatusCode(HttpStatusCode.NoContent);

        default:
            return NotFound();

    }
}

在此範例中,導覽屬性是單一 Supplier 實體。 如果導覽屬性是集合,則刪除連結的 URI 必須包含相關實體的金鑰。 例如:

DELETE /odata/Customers(1)/$links/Orders(1)

此請求從客戶 1 中刪除訂單 1。 在這種情況下,DeleteLink 方法將具有以下簽章:

void DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty);

relatedKey 參數給出相關實體的鍵。 因此,在您的 DeleteLink 方法中,透過金鑰參數尋找主要實體,透過 relatedKey 參數尋找相關實體,然後刪除關聯。 根據您的資料模型,您可能需要實作兩個版本的 DeleteLink。 Web API 將根據請求 URI 呼叫正確的版本。