使用 ASP.NET Web API 2.2 OData v4 中的实体关系Entity Relations in OData v4 Using ASP.NET Web API 2.2

通过Mike Wassonby Mike Wasson

大多数的数据集定义实体之间的关系:客户下了订单;书有作者;产品有供应商。Most data sets define relations between entities: Customers have orders; books have authors; products have suppliers. 使用 OData,可以通过实体关系导航的客户端。Using OData, clients can navigate over entity relations. 给定产品,您可以找到供应商。Given a product, you can find the supplier. 此外可以创建或删除关系。You can also create or remove relationships. 例如,可以设置一种产品的供应商。For example, you can set the supplier for a product.

本教程演示如何支持使用 ASP.NET Web API OData v4 中的这些操作。This tutorial shows how to support these operations in OData v4 using ASP.NET Web API. 本教程以教程为基础创建 OData v4 终结点使用 ASP.NET Web API 2The tutorial builds on the tutorial Create an OData v4 Endpoint Using ASP.NET Web API 2.

在本教程中使用的软件版本Software versions used in the tutorial

  • Web API 2.1Web API 2.1
  • OData v4OData v4
  • Visual Studio 2013 (下载 Visual Studio 2017此处)Visual Studio 2013 (download Visual Studio 2017 here)
  • Entity Framework 6Entity Framework 6
  • .NET 4.5.NET 4.5

教程版本Tutorial versions

OData 版本 3,请参阅支持 OData v3 中的实体关系For the OData Version 3, see Supporting Entity Relations in OData v3.

添加 Supplier 实体Add a Supplier Entity

Note

本教程以教程为基础创建 OData v4 终结点使用 ASP.NET Web API 2The tutorial builds on the tutorial Create an OData v4 Endpoint Using ASP.NET Web API 2.

首先,我们需要相关的实体。First, we need a related entity. 添加一个名为类Supplier模型文件夹中。Add a class named Supplier in the Models folder.

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类:Add a navigation property to the Product class:

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

添加一个新DbSetProductsContext类,以便实体框架将在数据库中包含的供应商表。Add a new DbSet to the ProductsContext class, so that Entity Framework will include the Supplier table in the database.

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 中,添加"供应商"实体设置为实体数据模型:In WebApiConfig.cs, add a "Suppliers" entity set to the entity data model:

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());
}

添加供应商控制器Add a Suppliers Controller

添加SuppliersController类到 Controllers 文件夹。Add a SuppliersController class to the Controllers folder.

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 操作。I won't show how to add CRUD operations for this controller. 步骤是产品控制器一样 (请参阅创建 OData v4 终结点)。The steps are the same as for the Products controller (see Create an OData v4 Endpoint).

若要获取产品的供应商,客户端发送 GET 请求:To get the supplier for a product, the client sends a GET request:

GET /Products(1)/Supplier

若要支持此请求,添加以下方法ProductsController类:To support this request, add the following method to the ProductsController class:

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.
}

此方法使用默认命名约定This method uses a default naming convention

  • 方法名称:GetX,其中 X 是导航属性。Method name: GetX, where X is the navigation property.
  • 参数名称:密钥Parameter name: key

如果遵循以下命名约定,Web API 会自动将 HTTP 请求映射到控制器方法。If you follow this naming convention, Web API automatically maps the HTTP request to the controller method.

示例 HTTP 请求:Example HTTP request:

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 响应示例:Example HTTP response:

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"
}

在上一示例中,产品有一个供应商。In the previous example, a product has one supplier. 一个导航属性还可以返回一个集合。A navigation property can also return a collection. 以下代码获取供应商的产品:The following code gets the products for a supplier:

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>In this case, the method returns an IQueryable instead of a SingleResult<T>

示例 HTTP 请求:Example HTTP request:

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 响应示例:Example HTTP response:

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
    }
  ]
}

创建实体之间的关系Creating a Relationship Between Entities

OData 支持创建或删除现有的两个实体之间的关系。OData supports creating or removing relationships between two existing entities. 在 OData v4 术语中,关系是"引用"。In OData v4 terminology, the relationship is a "reference". (在 OData v3,称为关系链接(In OData v3, the relationship was called a link. 协议差异没有影响此教程。)The protocol differences don't matter for this tutorial.)

引用具有自己的 URI,处理该窗体/Entity/NavigationProperty/$refA reference has its own URI, with the form /Entity/NavigationProperty/$ref. 例如,下面是用于解决产品和其供应商之间的引用的 URI:For example, here is the URI to address the reference between a product and its supplier:

http:/host/Products(1)/Supplier/$ref

若要添加关系时,客户端将 POST 或 PUT 请求发送到此地址。To add a relationship, the client sends a POST or PUT request to this address.

  • 如果导航属性是一个单一实体,如Product.SupplierPUT if the navigation property is a single entity, such as Product.Supplier.
  • 如果导航属性是一个集合,如发布Supplier.ProductsPOST if the navigation property is a collection, such as Supplier.Products.

请求的正文包含关系中的其他实体的 URI。The body of the request contains the URI of the other entity in the relation. 下面是示例请求:Here is an example request:

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,即为 $ref URISupplier的产品 id = 6。In this example, the client sends a PUT request to /Products(6)/Supplier/$ref, which is the $ref URI for the Supplier of the product with ID = 6. 如果请求成功,服务器会发送 204 (无内容) 响应:If the request succeeds, the server sends a 204 (No Content) response:

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

下面是要添加到关系的控制器方法Product:Here is the controller method to add a relationship to a 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参数指定要设置的关系。The navigationProperty parameter specifies which relationship to set. (如果该实体的多个导航属性,可以添加更多case语句。)(If there is more than one navigation property on the entity, you can add more case statements.)

链接参数包含供应商的 URI。The link parameter contains the URI of the supplier. Web API 会自动分析请求正文,以获取此参数的值。Web API automatically parses the request body to get the value for this parameter.

若要查找供应商,我们需要 ID (或密钥),这是的一部分链接参数。To look up the supplier, we need the ID (or key), which is part of the link parameter. 若要执行此操作,请使用以下帮助器方法:To do this, use the following helper method:

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 路径拆分成段,找到的段包含的密钥,然后将密钥转换为正确的类型。Basically, this method uses the OData library to split the URI path into segments, find the segment that contains the key, and convert the key into the correct type.

删除实体之间的关系Deleting a Relationship Between Entities

若要删除关系,客户端将 HTTP DELETE 请求发送到 $ref URI 中:To delete a relationship, the client sends an HTTP DELETE request to the $ref URI:

DELETE http://host/Products(1)/Supplier/$ref

下面是要删除某个产品和供应商之间的关系的控制器方法:Here is the controller method to delete the relationship between a Product and a 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"末尾 1 对多关系,以便可以删除此关系只需通过设置Product.SuppliernullIn this case, Product.Supplier is the "1" end of a 1-to-many relation, so you can remove the relationship just by setting Product.Supplier to null.

在中"许多"末尾的关系,客户端必须指定哪些相关实体中删除。In the "many" end of a relationship, the client must specify which related entity to remove. 若要执行此操作,客户端发送请求的查询字符串中的相关实体的 URI。To do so, the client sends the URI of the related entity in the query string of the request. 例如,若要删除"产品 1"从"供应商 1":For example, to remove "Product 1" from "Supplier 1":

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

若要在 Web API 中支持此功能,我们需要包括中的额外参数DeleteRef方法。To support this in Web API, we need to include an extra parameter in the DeleteRef method. 下面是要从产品中删除的控制器方法Supplier.Products关系。Here is the controller method to remove a product from the Supplier.Products relation.

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.
}

参数是供应商的键和relatedKey参数是要从中删除从产品的关键Products关系。The key parameter is the key for the supplier, and the relatedKey parameter is the key for the product to remove from the Products relationship. 请注意,Web API 会自动从查询字符串获取的密钥。Note that Web API automatically gets the key from the query string.