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

向 类添加新的 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 调用正确的版本。