ASP.NET Core OData 实践——Lesson7使用Reference增删改查一对一Navigation Property(C#)

在 OData 协议中,Reference(引用) 是一种专门用于管理实体之间关系的机制。它允许客户端通过标准的 RESTful 操作(如 POST、PUT、DELETE)直接操作实体之间的导航属性,而不是修改整个实体对象。这种方式非常适合处理如“订单-客户”、“员工-主管”等一对一或一对多的关联关系。

我们在《ASP.NET Core OData 实践——Lesson4增删改查Navigation Property(C#)》中接触的导航属性,就是实体之间的关系。我们也可以使用该文中的方法对它们进行管理。但是Reference机制是 OData 专门为“只操作关系”设计的标准接口,语义更明确,路由和元数据也有专门的支持。
举一个例子,我们可以通过Reference机制,让“订单”的“客户”属性设置为null,但不能修改“客户”自身的属性;如果直接操作导航属性,是可以通过Navigation Property的PATCH指令修改“订单”对应“客户”的自身属性,比如将“客户姓名”从“Tom”改成“Bill”,但这就不是Reference机制了。

为了更清晰分析和研究该特性,我们将该特性分为“一对一”和“一对多”两篇文章进行讲解。本文就以“订单-客户”这样的“一对一”案例进行讲解,其中“订单”是“引用对象”, “客户”是被引用对象。

主要模型设计

在项目下新增Models文件夹,并添加Customer、EnterpriseCustomer和Order三个类。

namespace Lesson7.Models
{
    public class Customer
    {
        public int Id { get; set; }
        public required string Name { get; set; }
    }
}
using System.IO;

namespace Lesson7.Models
{
    public class ThirdpartyPaymentOrder : Order
    {
        public Customer? PaidByCustomer { get; set; } // 代付的客户
    }
}
namespace Lesson7.Models
{
    public class Order
    {
        public int Id { get; set; }
        public decimal Amount { get; set; }
        public Customer? Customer { get; set; }
    }
}

在这里插入图片描述

Order引用了Customer,即有一个导航属性,表示该订单是所属者;
ThirdpartyPaymentOrder 是Order的派生类,表示一个代付订单。它多了一个PaidByCustomer 导航属性。

支持的接口

OData $ref 设计用于管理实体之间的关系(如添加、删除、替换引用),标准只定义了 GET、POST、PUT、DELETE 方法。
OData Reference($ref)操作本身不支持 PATCH 请求。这是因为PATCH是局部更新,而Reference不能更新“被引用对象”的属性,它只修改“被引用对象”和“引用对象”之间的关系;
对于不存在的“被引用对象”,我们可以通过POST或者PUT方法新建;如果“被引用对象”已经存在,则可以直接和“引用对象”建立关系。

以下是“一对一”和“一对多”都支持的接口

Request MethodRoute Template说明
GET~/{entityset}/{key}/{navigationproperty}/$ref查询基类类型Entity的导航属性
GET~/{entityset}/{key}/{cast}/{{navigationproperty}/$ref查询派生类型Entity的导航属性
GET~/{singleton}/{navigationproperty}/$ref查询基类类型单例的导航属性
GET~/{singleton}/{cast}/{navigationproperty}/$ref查询派生类型单例的导航属性
POST~/{entityset}/{key}/{navigationproperty}/{relatedkey}/$ref向基类类型Entity的导航属性设置一个新的Entity
POST~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
POST~/{entityset}/{key}/{navigationproperty}/$ref向基类类型Entity的导航属性设置一个新的Entity(Payload传递)
POST~/{entityset}/{key}/{cast}/{navigationproperty}/$ref向派生类型Entity的导航属性设置一个新的Entity(Payload传递)
POST~/{singleton}/{navigationproperty}/{relatedkey}/$ref向基类类型单例的导航属性设置一个新的Entity
POST~/{singleton}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型单例的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{navigationproperty}/{relatedkey}/$ref向基类类型Entity的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{navigationproperty}/$ref向基类类型Entity的导航属性设置一个新的Entity(Payload传递)
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/$ref向派生类类型Entity的导航属性设置一个新的Entity(Payload传递)
PUT~/{singleton}/{navigationproperty}/{relatedkey}/$ref向基类类型单例的导航属性设置一个新的Entity
PUT~/{singleton}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型单例的导航属性设置一个新的Entity
PUT~/{singleton}/{navigationproperty}/$ref向基类类型单例的导航属性设置一个新的Entity(Payload传递)
DELETE~/{entityset}/{key}/{navigationproperty}/$ref将基类类型Entity的导航属性的值设置为空
DELETE~/{entityset}/{key}/{cast}/{navigationproperty}/$ref将派生类型Entity的导航属性的值设置为空
DELETE~/{singleton}/{navigationproperty}/$ref将基类类型单例的导航属性的值设置为空
DELETE~/{singleton}/{cast}/{navigationproperty}/$ref将派生类型单例的导航属性的值设置为空

我们看到Reference是没有PATCH指令,即不能局部更新。就是连集合型导航属性的PATCH指令也不支持。

控制器设计

在项目中新增Controller文件夹,然后添加OrdersController类。该类注册于ODataController,以便拥有如下能力:

  1. OData 路由支持
    继承 ODataController 后,控制器自动支持 OData 路由(如 /odata/Shapes(1)),可以直接响应 OData 标准的 URL 路径和操作。
  2. OData 查询参数支持
    可以使用 [EnableQuery] 特性,自动支持 $filter、$select、$orderby、$expand 等 OData 查询参数,无需手动解析。
  3. OData 响应格式
    返回的数据会自动序列化为 OData 标准格式(如 JSON OData),方便前端或其他系统消费。
  4. OData Delta 支持
    支持 Delta<T>、DeltaSet<T> 等类型,便于实现 PATCH、批量 PATCH 等 OData 特有的部分更新操作。
  5. 更丰富的 OData 语义
    继承后可方便实现实体集、实体、导航属性、复杂类型等 OData 语义,提升 API 的表达能力。
using Lesson7.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace Lesson7.Controllers
{
    public class OrdersController: ODataController
    {
    }
}

下面我们在该类中填充逻辑。

数据源

        private static List<Customer> customers = new List<Customer>
        {
            new Customer { Id = 1, Name = "Customer 1" },
            new Customer { Id = 2, Name = "Customer 2" },
        };

        private static List<Order> orders = new List<Order>
        {
            new Order { Id = 1, Amount = 80, Customer = customers.SingleOrDefault(c => c.Id == 1) },
            new OnlineOrder { Id = 2, Amount = 40, Customer = customers.SingleOrDefault(c => c.Id == 2), Platform = "Online", TrackingNumber = "12345", IsPaid = true },

        };

查询(GET)

Request MethodRoute Template说明
GET~/{entityset}/{key}/{navigationproperty}/$ref查询基类类型Entity的导航属性
GET~/{entityset}/{key}/{cast}/{{navigationproperty}/$ref查询派生类型Entity的导航属性
GET~/{singleton}/{navigationproperty}/$ref查询基类类型单例的导航属性
GET~/{singleton}/{cast}/{navigationproperty}/$ref查询派生类型单例的导航属性

查询基类类型Entity的导航属性

Request MethodRoute Template说明
GET~/{entityset}/{key}/{navigationproperty}/$ref查询基类类型Entity的导航属性
        public ActionResult<Customer> GetRefToCustomer([FromRoute] int key)
        {
            var order = orders.SingleOrDefault(d => d.Id.Equals(key));
            if (order == null)
            {
                return NotFound();
            }
            return Ok(order.Customer);
        }
  • Request
curl --location 'http://localhost:5119/odata/Orders(1)/Customer/$ref'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Lesson7.Models.Customer",
    "Id": 1,
    "Name": "Customer 1"
}

查询派生类型Entity的导航属性

Request MethodRoute Template说明
GET~/{entityset}/{key}/{cast}/{{navigationproperty}/$ref查询派生类型Entity的导航属性
        public ActionResult<Customer> GetRefToPaidByCustomerFromThirdpartyPaymentOrder([FromRoute] int key)
        {
            var order = orders.OfType<ThirdpartyPaymentOrder>().SingleOrDefault(d => d.Id.Equals(key));
            if (order == null)
            {
                return NotFound();
            }
            return Ok(order.PaidByCustomer);
        }
  • Request
curl --location 'http://localhost:5119/odata/Orders(2)/Lesson7.Models.ThirdpartyPaymentOrder/PaidByCustomer/$ref'

这儿需要注意派生类型和导航属性的关系。
如果导航属性定义在派生类型自身,而不是派生类型通过继承获得,则可以通过Reference机制访问到。比如本例中导航属性PaidByCustomer是定义在ThirdpartyPaymentOrder中的。
如果导航属性是派生类型通过继承形式获得,则不可以通过派生类型查询到该导航属性。比如本例中Order.Customer也是导航属性,ThirdpartyPaymentOrder通过继承于Order获得该属性,那不能通过URIodata/Orders(2)/Lesson7.Models.ThirdpartyPaymentOrder/Customer/$ref请求到Customer,哪怕定义了对应的方法GetRefToCustomerFromThirdpartyPaymentOrder也不行。

  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Lesson7.Models.Customer",
    "Id": 2,
    "Name": "Customer 2"
}

新增(POST)和 完整更新(PUT)

对于单值(一对一)导航属性(如 Order.Customer),OData 规范规定:

  • POST 和 PUT 到 /odata/Orders({key})/Customer/$ref 都表示“建立或替换”关系。
  • 两者的语义在单值导航属性下没有区别,都是把 Order.Customer 指向新的 Customer。

所以POST 和 PUT 都会路由到同一个方法(如 CreateRefToCustomer )。

Request MethodRoute Template说明
POST~/{entityset}/{key}/{navigationproperty}/{relatedkey}/$ref向基类类型Entity的导航属性设置一个新的Entity
POST~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
POST~/{entityset}/{key}/{navigationproperty}/$ref向基类类型Entity的导航属性设置一个新的Entity(Payload传递)
POST~/{entityset}/{key}/{cast}/{navigationproperty}/$ref向派生类型Entity的导航属性设置一个新的Entity(Payload传递)
POST~/{singleton}/{navigationproperty}/{relatedkey}/$ref向基类类型单例的导航属性设置一个新的Entity
POST~/{singleton}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型单例的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{navigationproperty}/{relatedkey}/$ref向基类类型Entity的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{navigationproperty}/$ref向基类类型Entity的导航属性设置一个新的Entity(Payload传递)
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/$ref向派生类类型Entity的导航属性设置一个新的Entity(Payload传递)
PUT~/{singleton}/{navigationproperty}/{relatedkey}/$ref向基类类型单例的导航属性设置一个新的Entity
PUT~/{singleton}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型单例的导航属性设置一个新的Entity
PUT~/{singleton}/{navigationproperty}/$ref向基类类型单例的导航属性设置一个新的Entity(Payload传递)

向基类类型Entity的导航属性建立或替换一个新的Entity

Request MethodRoute Template说明
POST~/{entityset}/{key}/{navigationproperty}/{relatedkey}/$ref向基类类型Entity的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{navigationproperty}/{relatedkey}/$ref向基类类型Entity的导航属性设置一个新的Entity
        public ActionResult CreateRefToCustomer([FromRoute] int key, [FromRoute] int relatedKey)
        {
            var order = orders.SingleOrDefault(d => d.Id.Equals(key));
            if (order == null)
            {
                return NotFound();
            }

            var customer = customers.SingleOrDefault(c => c.Id == relatedKey);
            if (customer == null)
            {
                customer = new Customer { Id = relatedKey, Name = $"Customer {relatedKey}" };
                customers.Add(customer);
                order.Customer = customer;

                return Ok(customer);
            }

            // Quick, lazy and dirty
            order.Customer = customer;

            return Ok(customer);
        }

CreateRefToCustomer根据订单主键 key 查找订单对象,然后根据客户主键 relatedKey 查找客户对象,如果不存在则新建并加入集合,最后将订单的 Customer 属性指向该客户,实现订单与客户的关联。

POST
  • Request
curl --location --request POST 'http://localhost:5119/odata/Orders(1)/Customer(11)/$ref'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Lesson7.Models.Customer",
    "Id": 11,
    "Name": "Customer 11"
}
PUT
curl --location --request PUT 'http://localhost:5119/odata/Orders(1)/Customer(12)/$ref'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Lesson7.Models.Customer",
    "Id": 12,
    "Name": "Customer 12"
}

向派生类型Entity的导航属性建立或替换一个新的Entity

Request MethodRoute Template说明
POST~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
        public ActionResult CreateRefToPaidByCustomerFromThirdpartyPaymentOrder([FromRoute] int key, [FromRoute] int relatedKey)
        {
            var order = orders.OfType<ThirdpartyPaymentOrder>().SingleOrDefault(d => d.Id.Equals(key));
            if (order == null)
            {
                return NotFound();
            }

            var customer = customers.SingleOrDefault(c => c.Id == relatedKey);
            if (customer == null)
            {
                customer = new Customer { Id = relatedKey, Name = $"Customer {relatedKey}" };
                customers.Add(customer);
                order.PaidByCustomer = customer;

                return Ok(customer);
            }

            // Quick, lazy and dirty
            order.PaidByCustomer = customer;

            return Ok(customer);
        }

CreateRefToPaidByCustomerFromThirdpartyPaymentOrder根据订单主键 key 查找对应的 ThirdpartyPaymentOrder 实例,然后根据客户主键 relatedKey 查找客户对象,如果不存在则新建并加入集合,最后将订单的 PaidByCustomer 属性指向该客户,实现“由谁代付”的关联。

POST
Request MethodRoute Template说明
POST~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
  • Request
curl --location --request POST 'http://localhost:5119/odata/Orders(2)/Lesson7.Models.ThirdpartyPaymentOrder/PaidByCustomer(13)/$ref'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Lesson7.Models.Customer",
    "Id": 13,
    "Name": "Customer 13"
}
PUT
Request MethodRoute Template说明
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/{relatedkey}/$ref向派生类型Entity的导航属性设置一个新的Entity
  • Request
curl --location --request PUT 'http://localhost:5119/odata/Orders(2)/Lesson7.Models.ThirdpartyPaymentOrder/PaidByCustomer(14)/$ref' 
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Lesson7.Models.Customer",
    "Id": 14,
    "Name": "Customer 14"
}

使用Payload的方式向导航属性设置一个新的Entity

Request MethodRoute Template说明
POST~/{entityset}/{key}/{navigationproperty}/$ref向基类类型Entity的导航属性设置一个新的Entity(Payload传递)
POST~/{entityset}/{key}/{cast}/{navigationproperty}/$ref向派生类型Entity的导航属性设置一个新的Entity(Payload传递)
PUT~/{entityset}/{key}/{navigationproperty}/$ref向基类类型Entity的导航属性设置一个新的Entity(Payload传递)
PUT~/{entityset}/{key}/{cast}/{navigationproperty}/$ref向派生类类型Entity的导航属性设置一个新的Entity(Payload传递)

《ASP.NET Core OData 实践——Lesson7通过Payload修改Reference(C#)》

删除(DELETE)

Request MethodRoute Template说明
DELETE~/{entityset}/{key}/{navigationproperty}/$ref将基类类型Entity的导航属性的值设置为空
DELETE~/{entityset}/{key}/{cast}/{navigationproperty}/$ref将派生类型Entity的导航属性的值设置为空
DELETE~/{singleton}/{navigationproperty}/$ref将基类类型单例的导航属性的值设置为空
DELETE~/{singleton}/{cast}/{navigationproperty}/$ref将派生类型单例的导航属性的值设置为空

将基类类型Entity的导航属性的值设置为空

        public ActionResult DeleteRefToCustomer([FromRoute] int key)
        {
            var order = orders.SingleOrDefault(d => d.Id.Equals(key));

            if (order == null)
            {
                return NotFound();
            }

            order.Customer = null!; // Use null-forgiving operator to explicitly allow null assignment.

            return NoContent();
        }
  • Request
curl --location --request DELETE 'http://localhost:5119/odata/Orders(1)/Customer/$ref'

将派生类型Entity的导航属性的值设置为空

        public ActionResult DeleteRefToPaidByCustomerFromThirdpartyPaymentOrder([FromRoute] int key)
        {
            var order = orders.OfType<ThirdpartyPaymentOrder>().SingleOrDefault(d => d.Id.Equals(key));

            if (order == null)
            {
                return NotFound();
            }

            order.PaidByCustomer = null!; // Use null-forgiving operator to explicitly allow null assignment.

            return NoContent();
        }
  • Request
curl --location --request DELETE 'http://localhost:5119/odata/Orders(2)/Lesson7.Models.ThirdpartyPaymentOrder/PaidByCustomer/$ref' 

主程序

Reference机制并不需要在主程序中进行特殊设置。

using Lesson7.Models;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
using Microsoft.OData.Edm;

var builder = WebApplication.CreateBuilder(args);

// 提取 OData EDM 模型构建为方法,便于维护和扩展
static IEdmModel GetEdmModel()
{
    var modelBuilder = new ODataConventionModelBuilder();
    modelBuilder.EntitySet<Order>("Orders");

    return modelBuilder.GetEdmModel();
}

// 添加 OData 服务和配置
builder.Services.AddControllers().AddOData(options =>
    options.Select()
           .Filter()
           .OrderBy()
           .Expand()
           .Count()
           .SetMaxTop(null)
           .AddRouteComponents("odata", GetEdmModel())
);

var app = builder.Build();

app.UseRouting();

app.MapControllers();

app.Run();

服务文档

  • Request
curl --location 'http://localhost:5119/odata'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata",
    "value": [
        {
            "name": "Orders",
            "kind": "EntitySet",
            "url": "Orders"
        }
    ]
}

模型元文档

  • Request
curl --location 'http://localhost:5119/odata/$metadata'
  • Response
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
        <Schema Namespace="Lesson7.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Order">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Amount" Type="Edm.Decimal" Nullable="false" Scale="variable" />
                <NavigationProperty Name="Customer" Type="Lesson7.Models.Customer" />
            </EntityType>
            <EntityType Name="Customer">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Name" Type="Edm.String" Nullable="false" />
            </EntityType>
            <EntityType Name="ThirdpartyPaymentOrder" BaseType="Lesson7.Models.Order">
                <NavigationProperty Name="PaidByCustomer" Type="Lesson7.Models.Customer" />
            </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <EntitySet Name="Orders" EntityType="Lesson7.Models.Order" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

代码地址

https://github.com/f304646673/odata/tree/main/csharp/Lesson/Lesson7

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值