ASP.NET Core OData 实践——Lesson2增删改查Entity(C#)

在 OData(开放数据协议)中,Entity(实体) 是数据模型的核心单元,代表现实世界中可唯一标识的 “事物”(如客户、订单、产品等)。它是 OData 服务与客户端交互的基本数据载体,通过标准化的结构和协议实现跨系统的数据共享。

特点

  1. 唯一标识
    每个实体都有唯一的主键(如 Id),用于区分不同对象。
  2. 属性
    实体包含一组属性(如 Shape 的 Area,Circle 的 Radius),描述其业务特征。
  3. 类型层次
    实体可以有继承关系。例如,Circle 和 Rectangle 都继承自 Shape,体现了多态性。
  4. 可操作性
    实体支持标准的 CRUD 操作(增删改查),如:
  • GET /odata/Shapes(1) 查询单个实体
  • PUT /odata/Shapes(1) 替换实体
  • PATCH /odata/Shapes(1) 局部更新
  • DELETE /odata/Shapes(1) 删除实体
  1. 可扩展性
    可以通过继承和扩展属性,灵活适应不同业务需求。

支持的接口

Entity

Request MethodRoute Template说明
GET~/{entityset}/{key}查询实体集中指定Key的基类类型Entity
GET~/{entityset}({key})查询实体集中指定Key的基类类型Entity
GET~/{entityset}/{key}/{cast}查询实体集中指定Key的派生类类型Entity
GET~/{entityset}({key})/{cast}查询实体集中指定Key的派生类类型Entity
PUT~/{entityset}/{key}更新实体集中指定Key的基类类型Entity
PUT~/{entityset}({key})更新实体集中指定Key的基类类型Entity
PUT~/{entityset}/{key}/{cast}更新实体集中指定Key的派生类类型Entity
PUT~/{entityset}({key})/{cast}更新实体集中指定Key的派生类类型Entity
PATCH~/{entityset}/{key}更新实体集中指定Key的基类类型Entity的部分Property
PATCH~/{entityset}({key})更新实体集中指定Key的基类类型Entity的部分Property
PATCH~/{entityset}/{key}/{cast}更新实体集中指定Key的派生类类型Entity的部分Property
PATCH~/{entityset}({key})/{cast}更新实体集中指定Key的派生类类型Entity的部分Property
DELETE~/{entityset}/{key}删除实体集中指定Key的基类类型Entity
DELETE~/{entityset}({key})删除实体集中指定Key的基类类型Entity
DELETE~/{entityset}/{key}/{cast}删除实体集中指定Key的派生类类型Entity
DELETE~/{entityset}({key})/{cast}删除实体集中指定Key的派生类类型Entity

主要模型设计(Models)

  • Shape:基础形状类,包含 Id 和 Area 属性。
  • Circle:继承自 Shape,增加 Radius 属性。
  • Rectangle:继承自 Shape,增加 Length 和 Width 属性。

这种设计体现了面向对象的继承关系,方便在 OData 查询和操作时支持多态。
我们在工程中新增Models目录,并添加Shape、Circle和Rectangle类文件。
在这里插入图片描述
对应文件填入以下内容:

namespace Lesson2.Models
{
    public class Shape
    {
        public int Id { get; set; }
        public double Area { get; set; }
    }
}
namespace Lesson2.Models
{
    public class Circle : Shape
    {
        public double Radius { get; set; }
    }
}
namespace Lesson2.Models
{
    public class Rectangle : Shape
    {
        public double Length { get; set; }
        public double Width { get; set; }
    }
}

控制器设计(ShapesController)

在项目下新建Controller目录,其下新增一个ShapesController类。该类注册于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 Lesson2.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using System.Reflection;

namespace Lesson2.Controllers
{
    public class ShapesController : ODataController
    {
    ……
    }
}

下面我们将在ShapesController 内填充代码

数据源

private static List<Shape> shapes =
    [
        new Shape { Id = 1, Area = 28 },
        new Circle { Id = 2, Radius = 3.5, Area = 38.5 },
        new Rectangle { Id = 3, Length = 8, Width = 5, Area = 40 }
    ];

使用静态 List 存储所有形状对象,便于演示和测试。

新增(POST)

Entity需要存在于EntitySet之中,所以新增Entity是要在EntitySet的接口中。见《ASP.NET Core OData 实践——Lesson2查询和局部更新EntitySet(C#)》

查询(GET)

Request MethodRoute Template说明
GET~/{entityset}/{key}查询实体集中指定Key的基类类型Entity
GET~/{entityset}({key})查询实体集中指定Key的基类类型Entity
GET~/{entityset}/{key}/{cast}查询实体集中指定Key的派生类类型Entity
GET~/{entityset}({key})/{cast}查询实体集中指定Key的派生类类型Entity

查询实体集中指定Key的基类类型Entity

Request MethodRoute Template说明
GET~/{entityset}/{key}查询实体集中指定Key的基类类型Entity
GET~/{entityset}({key})查询实体集中指定Key的基类类型Entity
public ActionResult<Shape> Get([FromRoute] int key)
{
    var shape = shapes.SingleOrDefault(d => d.Id.Equals(key));

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

    return Ok(shape);
}

由于我们使用基类类型查询,可能得到基类或者其派生类的Entity。为了让我们可以在Response中准确知晓Entity的EntityType,OData响应中会在类型不一致时多返回@odata.type字段。即:

  • 只返回 @odata.context:

    • 当返回的对象类型与 EDM(元数据)中声明的类型一致时,只会有 @odata.context。
    • 例如,查询 Shapes 集合,返回的每个元素都是 Shape 类型或其子类,但声明类型为 Shape,且实际类型与声明类型一致时,不会额外返回 @odata.type。
  • 同时返回 @odata.context 和 @odata.type:

    • 当返回的对象是多态(派生类)实例,且实际类型与 EDM 声明类型不一致时,会同时返回 @odata.context 和 @odata.type。
    • 例如,查询 Shapes 集合,某个元素实际是 Circle 或 Rectangle,但声明类型为 Shape,此时该元素会包含 @odata.type,如 “#Lesson2.Models.Circle”,用于标明实际类型。
返回数据类型和请求类型一致
  • Request
curl --location 'http://localhost:5119/odata/Shapes(1)'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes/$entity",
    "Id": 1,
    "Area": 28.0
}
返回数据类型和请求类型不一致
  • Request
curl --location 'http://localhost:5119/odata/Shapes/2'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes/Lesson2.Models.Circle/$entity",
    "@odata.type": "#Lesson2.Models.Circle",
    "Id": 2,
    "Area": 38.5,
    "Radius": 3.5
}

查询实体集中指定Key的派生类类型Entity

Request MethodRoute Template说明
GET~/{entityset}/{key}/{cast}查询实体集中指定Key的派生类类型Entity
GET~/{entityset}({key})/{cast}查询实体集中指定Key的派生类类型Entity
public ActionResult<Circle> GetCircle([FromRoute] int key)
{
    var circle = shapes.OfType<Circle>().SingleOrDefault(d => d.Id.Equals(key));

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

    return Ok(circle);
}
  • Request
curl --location 'http://localhost:5119/odata/Shapes(2)/Lesson2.Models.Circle'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes/Lesson2.Models.Circle/$entity",
    "Id": 2,
    "Area": 38.5,
    "Radius": 3.5
}

完整更新Entity(PUT)

Request MethodRoute Template说明
PUT~/{entityset}/{key}更新实体集中指定Key的基类类型Entity
PUT~/{entityset}({key})更新实体集中指定Key的基类类型Entity
PUT~/{entityset}/{key}/{cast}更新实体集中指定Key的派生类类型Entity
PUT~/{entityset}({key})/{cast}更新实体集中指定Key的派生类类型Entity

• 语义:整体替换。
• 用法:客户端需要提交整个对象的所有属性(即使只改了一个字段,也要把所有字段都发过来)。
• 行为:服务器用客户端提交的对象完全替换现有对象,未提交的字段会被重置为默认值

更新实体集中指定Key的基类类型Entity

Request MethodRoute Template说明
PUT~/{entityset}/{key}更新实体集中指定Key的基类类型Entity
PUT~/{entityset}({key})更新实体集中指定Key的基类类型Entity
public ActionResult Put([FromRoute] int key, [FromBody] Shape shape)
{
    var item = shapes.SingleOrDefault(d => d.Id.Equals(key));

    if (shape == null)
    {
        return BadRequest("Shape cannot be null.");
    }
    else if (item == null)
    {
        return NotFound();
    }
    else if (!item.GetType().Equals(shape.GetType()))
    {
        return BadRequest();
    }

    // Update properties using reflection
    foreach (var propertyInfo in shape.GetType().GetProperties(
        BindingFlags.Public | BindingFlags.Instance))
    {
        var itemPropertyInfo = item.GetType().GetProperty(
            propertyInfo.Name,
            BindingFlags.Public | BindingFlags.Instance);

        // Ensure itemPropertyInfo is not null before accessing CanWrite
        if (itemPropertyInfo != null && itemPropertyInfo.CanWrite)
        {
            itemPropertyInfo.SetValue(item, propertyInfo.GetValue(shape));
        }
    }

    return Ok();
}

由于我们使用基类类型更新Entity,其存在两种情况:

  • Entity类型和请求路径中类型一致
  • Entity类型和请求路径中类型不一致

所以我们在代码中需要判断找到的对象类型(item.GetType())和待更新的对象类型(shape.GetType())是否一致。只有一致的情况下才进行修改。
那么如何辨别作为传入参数的shape变量的类型呢?这就需要我们在Reques中指定@odata.type

Entity类型和请求路径中类型一致

由于Id为1的对象就是方法入参类型(Shape),所以不用在Request中传递@odata.type

  • Request
curl --location --request PUT 'http://localhost:5119/odata/Shapes/1' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 1,
    "Area": 48
}'
Entity类型和请求路径中类型不一致

Id为3的对象的类型是Rectangle,是方法入参类型(Shape)的派生类,这就需要在Request中传递@odata.type

  • Request
curl --location --request PUT 'http://localhost:5119/odata/Shapes(3)' \
--header 'Content-Type: application/json' \
--data-raw '{
    "@odata.type": "#Lesson2.Models.Rectangle",
    "Id": 3,
    "Length": 8,
    "Width": 6,
    "Area": 48
}'

更新实体集中指定Key的派生类类型Entity

Request MethodRoute Template说明
PUT~/{entityset}/{key}/{cast}更新实体集中指定Key的派生类类型Entity
PUT~/{entityset}({key})/{cast}更新实体集中指定Key的派生类类型Entity

由于已经指定了特定派生类,这样我们就不需要像ActionResult Put([FromRoute] int key, [FromBody] Shape shape)方法那样使用反射的机制修改数据,只要通过指定数据修改即可。

public ActionResult PutCircle([FromRoute] int key, [FromBody] Circle circle)
{
    var item = shapes.OfType<Circle>().SingleOrDefault(d => d.Id.Equals(key));

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

    item.Id = circle.Id;
    item.Radius = circle.Radius;
    item.Area = circle.Area;

    return Ok();
}
  • Request
curl --location --request PUT 'http://localhost:5119/odata/Shapes(2)/Lesson2.Models.Circle' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 2,
    "Radius": 0.7,
    "Area": 1.54
}'

局部更新(PATCH)

Put和Patch都是更新,但是Put是整体更新,Patch是局部更新。
对于Entity,Patch请求只需要发送需要更改的Property的内容,其他Property的值保持不变。

Request MethodRoute Template说明
PATCH~/{entityset}/{key}更新实体集中指定Key的基类类型Entity的部分Property
PATCH~/{entityset}({key})更新实体集中指定Key的基类类型Entity的部分Property
PATCH~/{entityset}/{key}/{cast}更新实体集中指定Key的派生类类型Entity的部分Property
PATCH~/{entityset}({key})/{cast}更新实体集中指定Key的派生类类型Entity的部分Property

通过基类类型进行局部更新

public ActionResult Patch([FromRoute] int key, [FromBody] Delta<Shape> delta)
{
    var shape = shapes.SingleOrDefault(d => d.Id.Equals(key));

    if (shape == null)
    {
        return NotFound();
    }
    else if (delta == null)
    {
        return BadRequest();
    }
    else if (!shape.GetType().Equals(delta.StructuredType))
    {
        return BadRequest();
    }

    delta.Patch(shape);

    return Ok();
}
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Shapes(3)' \
--header 'Content-Type: application/json' \
--data-raw '{
    "@odata.type": "#Lesson2.Models.Rectangle",
    "Width": 8,
    "Area": 64
}'

由于要更新的Entity的EntityType是Rectangle,而不是基类类型Shape,所以我们需要通过@odata.type指定传递数据的准确类型。

通过指定派生类类型进行局部更新

public ActionResult PatchCircle([FromRoute] int key, [FromBody] Delta<Circle> delta)
{
    var shape = shapes.OfType<Circle>().SingleOrDefault(d => d.Id.Equals(key));

    if (shape == null)
    {
        return NotFound();
    }
    else if (delta == null)
    {
        return BadRequest();
    }
    delta.Patch(shape);

    return Ok();
}
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Shapes(2)/Lesson2.Models.Circle' \
--header 'Content-Type: application/json' \
--data '{
    "Radius": 1.4,
    "Area": 6.16
}'

由于要更新的Entity的EntityType是Circle,且URI路径中也是指定了该派生类类型,所以这次我们不用传递@odata.type来声明数据的类型。

删除Entity(DELETE)

Request MethodRoute Template说明
DELETE~/{entityset}/{key}删除实体集中指定Key的基类类型Entity
DELETE~/{entityset}({key})删除实体集中指定Key的基类类型Entity
DELETE~/{entityset}/{key}/{cast}删除实体集中指定Key的派生类类型Entity
DELETE~/{entityset}({key})/{cast}删除实体集中指定Key的派生类类型Entity

删除数据需要指定Key,而Key存在于基类类型。这样我们既可以通过基类类型删除对象,还可以通过派生类类型删除对象。

通过基类类型删除对象

public ActionResult DeleteShape([FromRoute] int key)
{
    var shape = shapes.SingleOrDefault(d => d.Id.Equals(key));

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

    shapes.Remove(shape);

    return NoContent();
}

通过派生类类型删除对象

        public ActionResult DeleteCircle([FromRoute] int key)
        {
            var shape = shapes.OfType<Circle>().SingleOrDefault(d => d.Id.Equals(key));

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

            shapes.Remove(shape);

            return NoContent();
        }

主程序

我们只用将基类类型注册为EntitySet,而不用注册其派生类类型。

using Lesson2.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<Shape>("Shapes");
    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": "Shapes",
            "kind": "EntitySet",
            "url": "Shapes"
        }
    ]
}

模型元文档

  • 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="Lesson2.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Shape">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Area" Type="Edm.Double" Nullable="false" />
            </EntityType>
            <EntityType Name="Circle" BaseType="Lesson2.Models.Shape">
                <Property Name="Radius" Type="Edm.Double" Nullable="false" />
            </EntityType>
            <EntityType Name="Rectangle" BaseType="Lesson2.Models.Shape">
                <Property Name="Length" Type="Edm.Double" Nullable="false" />
                <Property Name="Width" Type="Edm.Double" Nullable="false" />
            </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <EntitySet Name="Shapes" EntityType="Lesson2.Models.Shape" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

它包含如下几部分:

  1. 实体类型定义
  • Shape
    • EntityType Name=“Shape”:定义了一个名为 Shape 的实体类型。
    • <Key><PropertyRef Name=“Id” /></Key>:以 Id 属性作为主键。
    • <Property Name=“Id” Type=“Edm.Int32” Nullable=“false” />:Id 是 int 类型,不能为空。
    • <Property Name=“Area” Type=“Edm.Double” Nullable=“false” />:Area 是 double 类型,不能为空。
  • Circle
    • EntityType Name=“Circle” BaseType=“Lesson2.Models.Shape”:Circle 继承自 Shape。
    • <Property Name=“Radius” Type=“Edm.Double” Nullable=“false” />:新增属性 Radius,double 类型,不能为空。
  • Rectangle
    • EntityType Name=“Rectangle” BaseType=“Lesson2.Models.Shape”:Rectangle 继承自 Shape。
    • <Property Name=“Length” Type=“Edm.Double” Nullable=“false” />:新增属性 Length,double 类型,不能为空。
    • <Property Name=“Width” Type=“Edm.Double” Nullable=“false” />:新增属性 Width,double 类型,不能为空。
  1. 实体容器与实体集
  • EntityContainer Name=“Container”:定义了一个实体容器,OData 服务的入口。
  • <EntitySet Name=“Shapes” EntityType=“Lesson2.Models.Shape” />:定义了一个名为 Shapes 的实体集,类型为 Shape。
    这意味着可以通过 /odata/Shapes 路由访问所有 Shape 及其派生类(Circle、Rectangle)对象。
  1. 继承关系
  • Circle 和 Rectangle 都通过 BaseType 继承自 Shape,拥有 Shape 的所有属性,并扩展了自己的属性。

代码地址

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

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

breaksoftware

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

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

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

打赏作者

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

抵扣说明:

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

余额充值