共用方式為


使用 ASP.NET Web API 2.2 的 OData v4 中的實體關係

演講者:Mike Wasson

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

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

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

  • Web API 2.1
  • OData v4
  • Visual Studio 2017 (在這裡下載 Visual Studio 2017)
  • Entity Framework 6
  • .NET 4.5

教學課程版本

對於 OData 版本 3,請參閱支援 OData v3 中的實體關係

新增供應商實體

注意

本教學課程是以「使用 ASP.NET Web API 2 建立 OData v4 端點」教學課程為基礎。

首先,我們需要一個相關實體。 新增一個在 Models 資料夾中命名的 Supplier 類別。

using System.Collections.Generic;

namespace ProductService.Models
{
    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }
}

Product 類別新增導覽屬性:

using System.ComponentModel.DataAnnotations.Schema;

namespace ProductService.Models
{
    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 int? SupplierId { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}

ProductsContext 類別中新增一個新的 DbSet,以便 Entity Framework 將在資料庫中包含供應商表。

public class ProductsContext : DbContext
{
    static ProductsContext()
    {
        Database.SetInitializer(new ProductInitializer());
    }

    public DbSet<Product> Products { get; set; }
    // New code:
    public DbSet<Supplier> Suppliers { get; set; }
}

在 WebApiConfig.cs 中,將「Suppliers」實體集新增至實體資料模型:

public static void Register(HttpConfiguration config)
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Product>("Products");
    // New code:
    builder.EntitySet<Supplier>("Suppliers");
    config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}

新增供應商控制器

將一個 SuppliersController 類別加入到 Controllers 資料夾中。

using ProductService.Models;
using System.Linq;
using System.Web.OData;

namespace ProductService.Controllers
{
    public class SuppliersController : ODataController
    {
        ProductsContext db = new ProductsContext();

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

我不會展示如何為此控制器新增 CRUD 操作。 步驟與產品控制器相同 (請參閱建立 OData v4 端點)。

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

GET /Products(1)/Supplier

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

public class ProductsController : ODataController
{
    // GET /Products(1)/Supplier
    [EnableQuery]
    public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
    {
        var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
        return SingleResult.Create(result);
    }
 
   // Other controller methods not shown.
}

此方法使用預設命名慣例

  • 方法名稱:GetX,其中 X 是導覽屬性。
  • 參數名稱:金鑰

如果遵循此命名慣例,Web API 會自動將 HTTP 請求對應到控制器方法。

HTTP 請求範例:

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 回應範例:

HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}

在前面的範例中,一種產品有一個供應商。 導覽屬性也可以傳回集合。 以下程式碼取得供應商的產品:

public class SuppliersController : ODataController
{
    // GET /Suppliers(1)/Products
    [EnableQuery]
    public IQueryable<Product> GetProducts([FromODataUri] int key)
    {
        return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
    }

    // Other controller methods not shown.
}

在這種情況下,該方法傳回 IQueryable 而不是 SingleResultT<>

HTTP 請求範例:

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 回應範例:

HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
    {
      "Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
    }
  ]
}

建立實體之間的關係

OData 支援建立或刪除兩個現有實體之間的關係。 在 OData v4 術語中,關係是「參考」。 (在 OData v3 中,這種關係稱為連結。協議差異對於本教學課程來說並不重要。)

參考有其自己的 URI,格式為 /Entity/NavigationProperty/$ref。 例如,以下是用於解決產品與其供應商之間的參考的 URI:

http:/host/Products(1)/Supplier/$ref

若要新增關係,用戶端將 POST 或 PUT 請求傳送到該位址。

  • 如果導覽屬性是單一實體 (例如 Product.Supplier),則 PUT。
  • 如果導覽屬性是集合,例如 Supplier.Products.POST

請求本文包含關係中其他實體的 URI。 這是一個請求範例:

PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue

{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}

在此範例中,用戶端將 PUT 請求傳送至 /Products(6)/Supplier/$ref,這是 ID = 6 的 Supplier 產品的 $ref URI。 如果請求成功,伺服器會傳送 204 (無內容) 回應:

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

以下是向 Product 新增關係的控制器方法:

public class ProductsController : ODataController
{
    [AcceptVerbs("POST", "PUT")]
    public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }
        switch (navigationProperty)
        {
            case "Supplier":
                // Note: The code for GetKeyFromUri is shown later in this topic.
                var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
                var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
                if (supplier == null)
                {
                    return NotFound();
                }

                product.Supplier = supplier;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();
        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

navigationProperty 參數指定要設定的關係。 (如果實體上有多個導覽屬性,您可以新增更多 case 陳述式。)

連結參數包含供應商的 URI。 Web API 會自動解析請求本文以取得該參數的值。

要查找供應商,我們需要 ID (或金鑰),它是連結參數的一部分。 為此,請使用以下輔助方法:

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;

namespace ProductService
{
    public static class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName, 
                request.ODataProperties().PathHandler, new List<ODataPathSegment>());
            var odataPath = request.ODataProperties().PathHandler.Parse(
                request.ODataProperties().Model, 
                serviceRoot, uri.LocalPath);

            var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
            return (TKey)value;
        }

    }
}

基本上,此方法使用 OData 庫將 URI 路徑拆分為段,找到包含金鑰的段,並將金鑰轉換為正確的類型。

刪除實體之間的關係

若要刪除關係,用戶端將 HTTP DELETE 請求傳送至 $ref URI:

DELETE http://host/Products(1)/Supplier/$ref

以下是刪除產品和供應商之間關係的控制器方法:

public class ProductsController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = db.Products.SingleOrDefault(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }

        switch (navigationProperty)
        {
            case "Supplier":
                product.Supplier = null;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }        

    // Other controller methods not shown.
}

在本例中,Product.Supplier 是一對多關係的「1」端,因此您可以透過設定 Product.Suppliernull 來刪除該關係。

在關係的「多」端,用戶端必須指定要刪除哪個相關實體。 為此,用戶端在請求的查詢字串中傳送相關實體的 URI。 例如,要從「供應商 1」中刪除「產品 1」:

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

為了在 Web API 中支援這一點,我們需要在 DeleteRef 方法中包含一個額外的參數。 這是從 Supplier.Products 關係中刪除產品的控制器方法。

public class SuppliersController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        [FromODataUri] string relatedKey, string navigationProperty)
    {
        var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
        if (supplier == null)
        {
            return StatusCode(HttpStatusCode.NotFound);
        }

        switch (navigationProperty)
        {
            case "Products":
                var productId = Convert.ToInt32(relatedKey);
                var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);

                if (product == null)
                {
                    return NotFound();
                }
                product.Supplier = null;
                break;
            default:
                return StatusCode(HttpStatusCode.NotImplemented);

        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

key 參數是供應商的鍵,relatedKey 參數是要從 Products 關係中刪除的產品的鍵。 請注意,Web API 會自動從查詢字串中取得金鑰。