使用 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.Supplier
null
即可删除关系。
在关系的“多”端,客户端必须指定要删除的相关实体。 为此,客户端会在请求的查询字符串中发送相关实体的 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 会自动从查询字符串获取密钥。