使用 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 如何处理除整数以外的其他键类型。
接下来,我们将通过将 属性添加到 Supplier
Product
类来创建关系:
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; }
}
向 类添加新的 DbSetProductServiceContext
,以便 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 中的密钥时使用 OData 语法规则。
创建和删除链接
OData 支持在两个实体之间创建或删除关系。 在 OData 术语中,关系是一个“链接”。每个链接都有一个 URI,其格式为 entity/$links/entity。 例如,从产品到供应商的链接如下所示:
/Products(1)/$links/Supplier
若要创建新链接,客户端会将 POST 请求发送到链接 URI。 请求的正文是目标实体的 URI。 例如,假设有一个供应商的密钥为“CTSO”。 若要创建从“Product (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();
}
}
该方法采用三个参数:
- key:产品 (父实体的密钥)
- 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 调用正确的版本。