使用 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 中的实体关系

添加供应商实体

首先,我们需要一个相关实体。 在 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; }
    }
}

将新的 DbSet 添加到 ProductsContext 类,以便 Entity Framework 将在数据库中包括 Supplier 表。

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 操作。 这些步骤与 Products 控制器的步骤相同 (请参阅 创建 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 是导航属性。
  • 参数名称: key

如果遵循此命名约定,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 而不是 SingleResult<T>

示例 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 请求。

  • 如果导航属性是单个实体,则为 PUT,例如 Product.Supplier
  • 如果导航属性是集合,则为 POST,例如 Supplier.Products

请求正文包含关系中另一个实体的 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 的产品的 $ref URI Supplier 。 如果请求成功,服务器将发送 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 语句。)

link 参数包含供应商的 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

下面是用于删除 Product 与 Supplier 之间关系的控制器方法:

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”中删除“Product 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 会自动从查询字符串获取密钥。