ASP.NET Core OData 实践——Lesson4增删改查Navigation Property(C#)


本文是一个基于 ASP.NET Core OData 的 Web API 示例,主要演示了如何对员工(Employee)和经理(Manager)进行标准的增删改查(CRUD)操作,并支持多态、导航属性(Navigation Property)和批量操作。

导航属性(Navigation Property)是实体类型(EntityType)中的属性(Property),其类型为另一个实体(Entity)或实体集合(EntitySet)。
如果Navigation Property是一个Entity,我们称之为SingleValuedNavigationProperty,即关联到 零个或一个目标实体 的导航属性,对应 OData 中的 EntityType 类型(可为空)。
如果Navigation Propert是一个EntitySet,我们称之为CollectionValuedNavigationProperty,即关联到 零个或多个目标实体 的导航属性,对应 OData 中的 Collection(EntityType) 类型。

支持的接口

Request MethodRoute Template说明
GET~/{entityset}/{key}/{navigationproperty}查询Entity的导航属性
GET~/{entityset}/{key}/{cast}/{navigationproperty}查询特定派生类类型Entity的导航属性
GET~/{entityset}/{key}/{cast}/{collectionvaluednavigationproperty}/$count查询特定派生类类型Entity的多值导航属性中值的个数
GET~/{singleton}/{navigationproperty}查询单例的导航属性
GET~/{singleton}/{cast}/{navigationproperty}查询特定派生类类型单例的导航属性
GET~/{singleton}/{cast}/{collectionvaluednavigationproperty}/$count查询特定派生类类型单例的多值导航属性中值的个数
POST~/{entityset}/{key}/{collectionvaluednavigationproperty}新增Entity的多值导航属性的值
POST~/{entityset}/{key}/{cast}/{collectionvaluednavigationproperty}新增特定派生类类型Entity的多值导航属性的值
POST~/{singleton}/{collectionvaluednavigationproperty}新增单例的多值导航属性的值
POST~/{singleton}/{cast}/{collectionvaluednavigationproperty}新增特定派生类类型单例的多值导航属性的值
PUT~/{entityset}/{key}/{singlevaluednavigationproperty}完整更新Entity的单值导航属性
PUT~/{entityset}/{key}/{cast}/{singlevaluednavigationproperty}完整更新特定派生类类型Entity的单值导航属性
PUT~/{singleton}/{singlevaluednavigationproperty}完整更新单例的单值导航属性
PUT~/{singleton}/{cast}/{singlevaluednavigationproperty}完整更新特定派生类类型单例的单值导航属性
PATCH~/{entityset}/{key}/{navigationproperty}局部更新Entity的导航属性
PATCH~/{entityset}/{key}/{cast}/{navigationproperty}局部更新特定派生类类型Entity的导航属性
PATCH~/{singleton}/{navigationproperty}局部更新单例的导航属性
PATCH~/{singleton}/{cast}/{navigationproperty}局部更新特定派生类类型单例的导航属性

主要模型设计

在项目下新增Models文件夹,并添加Employee和Manager类。
在这里插入图片描述

Employee是员工实体类型,包含:

  • Id
    类型:int
    说明:员工的唯一标识符。通常用于数据库主键或 OData 实体主键。
  • Name
    类型:required string
    说明:员工姓名。required 关键字表示创建对象时必须赋值,保证数据完整性。
  • Supervisor
    类型:Employee?
    说明:员工的直接上级(主管),类型为可空的 Employee。如果该员工没有主管(如公司最高领导),此属性为 null。这种自引用关系便于表达组织结构中的上下级关系。
  • Peers
    类型:List?
    说明:员工的同级同事集合。可空,默认为空列表。用于表示与该员工处于同一层级、同一部门或同一主管下的其他员工。初始化为 [](空列表),避免空引用异常。
namespace Lesson4.Models
{
    public class Employee
    {
        public int Id { get; set; }
        public required string Name { get; set; }
        public Employee? Supervisor { get; set; }
        public List<Employee>? Peers { get; set; } = [];
    }
}

Manager继承自 Employee,包含:

  • PersonalAssistant(私人助理)
    类型:Employee?(可空)
    说明:表示该经理的私人助理,可以为 null(即没有助理)。
    设计意义:体现了管理者与下属之间的一对一特殊关系。
  • DirectReports(直属下属)
    类型:List
    说明:表示该经理直接管理的员工列表,默认为空列表。
    设计意义:体现了组织结构中“经理-下属”的一对多关系,便于表达层级和导航查询。
namespace Lesson4.Models
{
    public class Manager : Employee
    {
        public Employee? PersonalAssistant { get; set; }
        public List<Employee> DirectReports { get; set; } = [];
    }
}

控制器设计

在项目中新增Controller文件夹,然后添加CompanyController类。该类注册于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 Lesson4.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace Lesson4.Controllers
{
    public class EmployeesController : ODataController
    {
    }
}

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

数据源

        private static IList<Employee> employees = GetEmployees();

        private static List<Employee> GetEmployees()
        {
            var employee5 = new Manager { Id = 5, Name = "Employee 5" };
            var employee1 = new Employee { Id = 1, Name = "Employee 1", Supervisor = employee5 };
            var employee2 = new Employee { Id = 2, Name = "Employee 2", Supervisor = employee5 };
            var employee3 = new Employee { Id = 3, Name = "Employee 3", Supervisor = employee5 };
            var employee4 = new Employee { Id = 4, Name = "Employee 4" }; // No Supervisor
            var employee6 = new Manager { Id = 6, Name = "Employee 6" };

            employee5.DirectReports = new List<Employee> { employee1, employee2, employee3 };
            employee5.PersonalAssistant = employee3;

            return new List<Employee> { employee1, employee2, employee3, employee4, employee5, employee6 };
        }
  • employees 是一个静态字段,保存所有员工和经理对象的内存集合,初始化时调用 GetEmployees() 方法。
  • GetEmployees() 方法用于构造初始的员工和经理数据:
    • 创建了 6 个员工对象,其中 Employee 5 和 Employee 6 是 Manager 类型,其余为普通 Employee。
    • Employee 1、Employee 2、Employee 3 的 Supervisor 都指向Employee 5,表示他们的上级是 Employee 5。
    • Employee 4 没有上级(Supervisor 为 null)。
    • Employee 5 的 DirectReports 包含 Employee 1、Employee 2、Employee 3,即这三人是其直属下属。
    • Employee 5 的 PersonalAssistant 设置为 Employee 3,即 Employee 3 是 Employee 5 的助理。
      最终返回一个包含所有员工和经理的列表,作为控制器的数据源。

查询(GET)

Request MethodRoute Template说明
GET~/{entityset}/{key}/{navigationproperty}查询Entity的导航属性
GET~/{entityset}/{key}/{cast}/{navigationproperty}查询特定派生类类型Entity的导航属性
GET~/{entityset}/{key}/{cast}/{collectionvaluednavigationproperty}/$count查询特定派生类类型Entity的多值导航属性中值的个数
GET~/{singleton}/{navigationproperty}查询单例的导航属性
GET~/{singleton}/{cast}/{navigationproperty}查询特定派生类类型单例的导航属性
GET~/{singleton}/{cast}/{collectionvaluednavigationproperty}/$count查询特定派生类类型单例的多值导航属性中值的个数
通过基类类型查询
        [EnableQuery]
        public ActionResult<IEnumerable<Employee>> Get()
        {
            return Ok(employees);
        }
普通查询
  • Request
curl --location 'http://localhost:5119/odata/Employees'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Employees",
    "value": [
        {
            "Id": 1,
            "Name": "Employee 1"
        },
        {
            "Id": 2,
            "Name": "Employee 2"
        },
        {
            "Id": 3,
            "Name": "Employee 3"
        },
        {
            "Id": 4,
            "Name": "Employee 4"
        },
        {
            "@odata.type": "#Lesson4.Models.Manager",
            "Id": 5,
            "Name": "Employee 5"
        },
        {
            "@odata.type": "#Lesson4.Models.Manager",
            "Id": 6,
            "Name": "Employee 6"
        }
    ]
}

可以看到Navigation Property的信息并不包含在返回结果中,且返回的Property只有基类类型Employee的Property。
由于"Employee 5"和"Employee 6"并不是基类类型,所以需要返回其具体类型——“#Lesson4.Models.Manager”。

查询Entity的Navigation Property
Request MethodRoute Template说明
GET~/{entityset}/{key}/{navigationproperty}查询Entity的导航属性
        public ActionResult<Employee> GetSupervisor([FromRoute] int key)
        {
            var employee = employees.SingleOrDefault(d => d.Id.Equals(key));

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

            if (employee.Supervisor == null)
            {
                return NotFound("The employee does not have a supervisor.");
            }

            return employee.Supervisor;
        }
  • Request
curl --location 'http://localhost:5119/odata/Employees(1)/Supervisor'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Employees/Lesson4.Models.Manager/$entity",
    "@odata.type": "#Lesson4.Models.Manager",
    "Id": 5,
    "Name": "Employee 5"
}

结果中返回"@odata.type",是因为我们无法通过请求URI得知Supervisor类型。
同时在返回的结果中,我们只看到Manager的基本属性,其Navigation Property仍然不返回。

查询特定派生类类型Entity的Navigation Property

Request MethodRoute Template说明
GET~/{entityset}/{key}/{cast}/{navigationproperty}查询特定派生类类型Entity的导航属性
        [EnableQuery]
        public ActionResult<IEnumerable<Employee>> GetDirectReportsFromManager([FromRoute] int key)
        {
            var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));

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

            return manager.DirectReports;
        }
  • Request

如果我们要查询一个特定派生类类型的一个Navigation Property,需要在URI中指定该派生类型的名称(Lesson4.Models.Manager),最后再加上Navigation Property Name(DirectReports)。

curl --location 'http://localhost:5119/odata/Employees(5)/Lesson4.Models.Manager/DirectReports'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Employees",
    "value": [
        {
            "Id": 1,
            "Name": "Employee 1"
        },
        {
            "Id": 2,
            "Name": "Employee 2"
        },
        {
            "Id": 3,
            "Name": "Employee 3"
        }
    ]
}

“Employee 1”、"Employee 2"和"Employee 3"都是Employees(EntitySet)的基本类型(EntityType)Employee的Entity。因为该URI是通过
Employees(EntitySet)请求的,所以其返回类型就是基本类型(EntityType),于是这些结果不需要使用 "@odata.type"额外说明。

查询多值Navigation Property的个数

Request MethodRoute Template说明
GET~/{entityset}/{key}/{cast}/{collectionvaluednavigationproperty}/$count查询特定派生类类型Entity的多值导航属性中值的个数

我们可以借用上面GetDirectReportsFromManager的方法,同时给该方法加上[EnableQuery]修饰,就可以自动计算其个数。

  • Request
curl --location 'http://localhost:5119/odata/Employees(5)/Lesson4.Models.Manager/DirectReports/$count'
  • Response
3

新增(POST)

Request MethodRoute Template说明
POST~/{entityset}/{key}/{collectionvaluednavigationproperty}新增Entity的多值导航属性的值
POST~/{entityset}/{key}/{cast}/{collectionvaluednavigationproperty}新增特定派生类类型Entity的多值导航属性的值
POST~/{singleton}/{collectionvaluednavigationproperty}新增单例的多值导航属性的值
POST~/{singleton}/{cast}/{collectionvaluednavigationproperty}新增特定派生类类型单例的多值导航属性的值

新增Entity的多值Navigation Property的值

        public ActionResult PostToPeers([FromRoute] int key, [FromBody] Employee peer)
        {
            var employee = employees.SingleOrDefault(d => d.Id.Equals(key));

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

            employees.Add(peer);
            if (employee.Peers == null)
            {
                employee.Peers = new List<Employee>();
            }
            employee.Peers.Add(peer);
            return Created(peer);
        }
Request MethodRoute Template说明
POST~/{entityset}/{key}/{collectionvaluednavigationproperty}新增Entity的多值导航属性的值
  • Request
curl --location 'http://localhost:5119/odata/Employees(4)/Peers' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 7,
    "Name": "Employee 7"
}'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Employees/$entity",
    "Id": 7,
    "Name": "Employee 7"
}

新增特定派生类类型Entity的多值Navigation Property的值

        public ActionResult PostToDirectReportsFromManager([FromRoute] int key, [FromBody] Employee employee)
        {
            var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));

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

            employees.Add(employee);
            manager.DirectReports.Add(employee);

            return Created(employee);
        }
Request MethodRoute Template说明
POST~/{entityset}/{key}/{cast}/{collectionvaluednavigationproperty}新增特定派生类类型Entity的多值导航属性的值
  • Request
curl --location 'http://localhost:5119/odata/Employees(6)/Lesson4.Models.Manager/DirectReports' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 8,
    "Name": "Employee 8"
}'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Employees/$entity",
    "Id": 8,
    "Name": "Employee 8"
}

完整更新(PUT)

Request MethodRoute Template说明
PUT~/{entityset}/{key}/{singlevaluednavigationproperty}完整更新Entity的单值导航属性
PUT~/{entityset}/{key}/{cast}/{singlevaluednavigationproperty}完整更新特定派生类类型Entity的单值导航属性
PUT~/{singleton}/{singlevaluednavigationproperty}完整更新单例的单值导航属性
PUT~/{singleton}/{cast}/{singlevaluednavigationproperty}完整更新特定派生类类型单例的单值导航属性

完整更新Navigation Property是指完全替换Entity中Navigation Property原来的Entity,而不是修改该Navigation Property的部分属性值。
举个例子,“Employee 1"的Supervisor是” Employee 5",完整更新就是指将Supervisor指向了" Employee 6"这类非原来Entity的对象。

完整更新Entity的单值导航属性

Request MethodRoute Template说明
PUT~/{entityset}/{key}/{singlevaluednavigationproperty}完整更新Entity的单值导航属性
        public ActionResult PutToSupervisor([FromRoute] int key, [FromBody] Employee supervisor)
        {
            var employee = employees.SingleOrDefault(d => d.Id.Equals(key));

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

            employee.Supervisor = supervisor;

            return Ok();
        }
  • Request
curl --location --request PUT 'http://localhost:5119/odata/Employees(1)/Supervisor' \
--header 'Content-Type: application/json' \
--data-raw '{
    "@odata.type": "#NavigationRouting.Models.Manager",
    "Id": 6,
    "Name": "Employee 6"
}
'

由于在请求的URI中并没有明确指明Supervisor的类型,所以需要在请求负载中用"@odata.type"指定。

完整更新特定派生类类型Entity的单值导航属

Request MethodRoute Template说明
PUT~/{entityset}/{key}/{cast}/{singlevaluednavigationproperty}完整更新特定派生类类型Entity的单值导航属性
        public ActionResult PutToPersonalAssistantFromManager([FromRoute] int key, [FromBody] Employee personalAssistant)
        {
            var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));

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

            manager.PersonalAssistant = personalAssistant;

            return Ok();
        }
  • Request
curl --location --request PUT 'http://localhost:5119/odata/Employees(6)/Lesson4.Models.Manager/PersonalAssistant' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 9,
    "Name": "Employee 9"
}
'

Manager的PersonalAssistant就是Employee类型,且Employ 9自身也是是Employee类型,所以不用在请求负载中设置"@odata.type"。

局部更新(PATCH)

Request MethodRoute Template说明
PATCH~/{entityset}/{key}/{navigationproperty}局部更新Entity的导航属性
PATCH~/{entityset}/{key}/{cast}/{navigationproperty}局部更新特定派生类类型Entity的导航属性
PATCH~/{singleton}/{navigationproperty}局部更新单例的导航属性
PATCH~/{singleton}/{cast}/{navigationproperty}局部更新特定派生类类型单例的导航属性

局部更新Navigation Property是指更新Navigation Property所指向的Entity的属性值。
举个例子,“Employee 1"的Supervisor是” Employee 5",局部更新就是指将" Employee 5"的Name属性修改成“Joe”这样的名字。

局部更新Entity的导航属性

Request MethodRoute Template说明
PATCH~/{entityset}/{key}/{navigationproperty}局部更新Entity的导航属性
        public ActionResult PatchToSupervisor([FromRoute] int key, [FromBody] Delta<Employee> delta)
        {
            var employee = employees.SingleOrDefault(d => d.Id.Equals(key));

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

            if (employee.Supervisor != null)
            {
                delta.Patch(employee.Supervisor);
            }

            return Ok();
        }
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Employees(1)/Supervisor' \
--header 'Content-Type: application/json' \
--data '{
    "Name": "Sue"
}'

局部更新特定派生类类型Entity的导航属性

Request MethodRoute Template说明
PATCH~/{entityset}/{key}/{cast}/{navigationproperty}局部更新特定派生类类型Entity的导航属性
        public ActionResult PatchToPersonalAssistantFromManager([FromRoute] int key, [FromBody] Delta<Employee> delta)
        {
            var manager = employees.OfType<Manager>().SingleOrDefault(d => d.Id.Equals(key));

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

            if (manager.PersonalAssistant != null)
            {
                delta.Patch(manager.PersonalAssistant);
            }

            return Ok();
        }
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Employees(6)/Lesson4.Models.Manager/PersonalAssistant' \
--header 'Content-Type: application/json' \
--data '{
    "Name": "Joe"
}
'

主程序

using Lesson4.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<Employee>("Employees");
    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": "Employees",
            "kind": "EntitySet",
            "url": "Employees"
        }
    ]
}

模型元文档

  • 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="Lesson4.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Employee">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Name" Type="Edm.String" Nullable="false" />
                <NavigationProperty Name="Supervisor" Type="Lesson4.Models.Employee" />
                <NavigationProperty Name="Peers" Type="Collection(Lesson4.Models.Employee)" />
            </EntityType>
            <EntityType Name="Manager" BaseType="Lesson4.Models.Employee">
                <NavigationProperty Name="PersonalAssistant" Type="Lesson4.Models.Employee" />
                <NavigationProperty Name="DirectReports" Type="Collection(Lesson4.Models.Employee)" />
            </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <EntitySet Name="Employees" EntityType="Lesson4.Models.Employee">
                    <NavigationPropertyBinding Path="Lesson4.Models.Manager/DirectReports" Target="Employees" />
                    <NavigationPropertyBinding Path="Lesson4.Models.Manager/PersonalAssistant" Target="Employees" />
                    <NavigationPropertyBinding Path="Peers" Target="Employees" />
                    <NavigationPropertyBinding Path="Supervisor" Target="Employees" />
                </EntitySet>
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

它包含如下几部分:

  1. 实体类型定义
  • Employee 实体
    * Id:主键,int 类型,不能为空。
    * Name:员工姓名,string 类型,不能为空。
    * Supervisor:导航属性,指向该员工的上级(也是 Employee 类型)。
    * Peers:导航属性,指向该员工的同级同事(Employee 集合)。
  • Manager 实体
    * 继承:Manager 继承自 Employee,拥有 Employee 的所有属性和导航属性。
    * PersonalAssistant:导航属性,指向经理的私人助理(Employee 类型)。
    * DirectReports:导航属性,指向经理的直属下属(Employee 集合)。
  1. 实体容器与实体集
  • EntitySet:定义了一个名为 Employees 的实体集,类型为 Employee。
  • NavigationPropertyBinding:将所有导航属性(包括 Manager 的扩展属性)都绑定到 Employees 集合,表示所有导航关系都在同一个实体集内查找目标
  1. 继承与多态
  • Manager 通过 BaseType 继承自 Employee,拥有所有 Employee 的属性和导航属性,并扩展了自己的导航属性。
  • 这种设计支持 OData 多态查询和导航,允许客户端通过 $expand 查询经理的下属、助理、同事、上级等复杂关系。

代码地址

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

参考资料

  • https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/navigation-routing?tabs=net60%2Cvisual-studio
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

breaksoftware

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

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

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

打赏作者

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

抵扣说明:

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

余额充值