使用 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 呼叫正確的版本。