使用 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.Supplier
為 null
來刪除該關係。
在關係的「多」端,用戶端必須指定要刪除哪個相關實體。 為此,用戶端在請求的查詢字串中傳送相關實體的 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 會自動從查詢字串中取得金鑰。