使用 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 摘要。 我們將新增 Supplier 類別。

using System.ComponentModel.DataAnnotations;

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

這個類別使用實體索引鍵的字串。 實際上,這可能比使用整數索引鍵還少。 但值得看看 OData 如何處理整數以外的其他索引鍵類型。

接下來,我們會將 屬性新增 SupplierProduct 類別來建立關聯:

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

將新的 DbSet 新增至 ProductServiceContext 類別,讓 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 中,將 「Suppliers」 實體新增至 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;
}

key參數是產品的索引鍵。 方法會傳回相關的實體,在此案例中為 Supplier 實例。 方法名稱和參數名稱都很重要。 一般而言,如果導覽屬性名為 「X」,您需要新增名為 「GetX」 的方法。 方法必須採用名為 「key」 的參數,以符合父索引鍵的資料類型。

key參數中包含[FromOdataUri]屬性也很重要。 當 Web API 從要求 URI 剖析金鑰時,此屬性會告訴 Web API 使用 OData 語法規則。

OData 支援建立或移除兩個實體之間的關聯性。 在 OData 術語中,關聯性是「連結」。每個連結都有一個 URI,其中包含表單 實體/$links/實體。 例如,從產品到供應商的連結看起來像這樣:

/Products(1)/$links/Supplier

若要建立新的連結,用戶端會將 POST 要求傳送至連結 URI。 要求的主體是目標實體的 URI。 例如,假設有一個供應商具有金鑰 「CTSO」。 若要從 「產品 (1) 」建立連結至「供應商 ('CTSO') 」,用戶端會傳送如下的要求:

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

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

若要刪除連結,用戶端會將 DELETE 要求傳送至連結 URI。

建立連結

若要讓用戶端建立產品供應商連結,請將下列程式碼新增至 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」。
  • link:相關實體的 OData URI。 此值取自要求本文。 例如,連結 URI 可能是 「 http://localhost/odata/Suppliers('CTSO') ,這表示識別碼為 'CTSO' 的供應商。

方法會使用 連結來查閱供應商。 如果找到相符的供應商,此方法會設定 屬性, Product.Supplier 並將結果儲存至資料庫。

最硬的部分是剖析連結 URI。 基本上,您需要模擬將 GET 要求傳送至該 URI 的結果。 下列協助程式方法示範如何執行這項操作。 方法會叫用 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 方法中,依 key 參數查閱主要實體、依 relatedKey 參數尋找相關實體,然後移除關聯。 視您的資料模型而定,您可能需要實作這兩個版本的 DeleteLink 。 Web API 會根據要求 URI 呼叫正確的版本。