ASP.NET Core OData 实践——Lesson2查询和局部更新EntitySet(C#)

EntitySet 是 OData 数据模型的核心载体,它通过标准化的 HTTP 接口和查询协议,实现了实体集合的高效操作与灵活扩展。无论是简单的 CRUD 场景,还是复杂的多态查询与动态字段支持,EntitySet 都为构建可扩展、易维护的 RESTful 服务提供了坚实基础。通过理解 EntitySet 的工作机制,我们能更好地设计 OData 服务的资源结构,提升 API 的可用性与性能。

概念

EntitySet 可以理解为数据库中的一张表,或者说是某种实体类型(EntityType)的集合。在 OData 服务中,每个 EntitySet 都会映射到一个控制器(Controller),用于处理对该集合的各种操作(如查询、添加、更新、删除等)。

多态

在 OData(开放数据协议)中,EntitySet 对多态的支持是其数据模型灵活性的重要体现。通过多态机制,EntitySet 可以包含基类实体及其派生类实体,允许开发者在不修改服务端元数据的情况下,扩展实体类型的行为与属性。
举个例子:我们定义一个基类 Shape(包含 Id 和 Area 属性),其派生类 Circle(新增 Radius 属性)和 Rectangle(新增 Width、Height 属性),三者可共同存储在 Shapes 这个 EntitySet 中。

支持的接口

EntitySet自身不可以新增,只能向其新增Entity。
由于Put是完整更新,所以EntitySet没有该接口。但是可以更新部分的Entity(局部修改),所以可以有Patch接口。
同时EntitySet自身也不能删除,所以也没有DELETE接口。

Request MethodRoute Template说明
GET~/{entityset}查询实体集中所有Entity
GET~/{entityset}/$count查询实体集中所有Entity的个数
GET~/{entityset}/{cast}查询实体集中派生类类型所有Entity
GET~/{entityset}/{cast}/$count查询实体集中派生类类型所有Entity的个数
POST~/{entityset}向实体集中新增基类类型Entity
POST~/{entityset}/{cast}向实体集中新增派生类类型Entity
PATCH~/{entityset}局部更新实体集中基类类型Entity
PATCH~/{entityset}/{cast}局部更新实体集中派生类类型Entityy

主要模型设计(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 存储所有形状对象,便于演示和测试。

查询(GET)

Request MethodRoute Template说明
GET~/{entityset}查询实体集中所有Entity
GET~/{entityset}/$count查询实体集中Entity的个数
GET~/{entityset}/{cast}查询实体集中派生类类型的Entity
GET~/{entityset}/{cast}/$count查询实体集中派生类类型所有Entity的个数

在下面的接口代码中,我们使用了Ok()方法。在正常情况下,也可以直接返回结果(return shapes),HTTP Response的内容和加了Ok方法(return Ok(shapes))一样,但是这依赖于框架的推导。这就导致某些情况下(如返回 null),没有加Ok()方法可能不会让框架自动包装为 404 或 204,易引发不一致的响应,所以强烈建议使用Ok方法。

我们还使用[EnableQuery]修饰了每个查询方法。这是因为这个特性让OData 控制器方法自动支持 OData 查询参数,如 $count、 $filter、$select、$orderby、$top、$skip 等。因为我们在实例中需要查询数量($count),所以使用了该修饰符。

查询EntitySet中所有Entity

Request MethodRoute Template说明
GET~/{entityset}查询实体集中所有Entity
[EnableQuery]
public ActionResult<IEnumerable<Shape>> Get()
{
    return Ok(shapes);
}

上述代码等同于

[EnableQuery]
public ActionResult<IEnumerable<Shape>> GetFromShape()
{
   return Ok(shapes.OfType<Shape>().ToList());
}
  • Request

下面请求对应于ActionResult<IEnumerable<Shape>> Get()

curl --location 'http://localhost:5119/odata/Shapes'

下面请求对应于ActionResult<IEnumerable<Shape>> GetFromShape()

curl --location 'http://localhost:5119/odata/Shapes/Lesson2.Models.Shape'
  • Response

它们返回的数据是一样

{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes",
    "value": [
        {
            "Id": 1,
            "Area": 28.0
        },
        {
            "@odata.type": "#Lesson2.Models.Circle",
            "Id": 2,
            "Area": 38.5,
            "Radius": 3.5
        },
        {
            "@odata.type": "#Lesson2.Models.Rectangle",
            "Id": 3,
            "Area": 40.0,
            "Length": 8.0,
            "Width": 5.0
        }
    ]
}

查询EntitySet中所有Entity的个数

Request MethodRoute Template说明
GET~/{entityset}/$count查询实体集中所有Entity的个数
  • Request
curl --location 'http://localhost:5119/odata/Shapes/$count'
  • Response
3

查询EntitySet中派生类类型的Entity

Request MethodRoute Template说明
GET~/{entityset}/{cast}查询实体集中派生类类型所有Entity

以查询Rectangle的EntitySet为例。

[EnableQuery]
public ActionResult<IEnumerable<Rectangle>> GetFromRectangle()
{
    return Ok(shapes.OfType<Rectangle>().ToList());
}
  • Request
curl --location 'http://localhost:5119/odata/Shapes/Lesson2.Models.Rectangle'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes/Lesson2.Models.Rectangle",
    "value": [
        {
            "Id": 3,
            "Area": 40.0,
            "Length": 8.0,
            "Width": 5.0
        }
    ]
}

查询EntitySet中派生类类型的Entity的个数

Request MethodRoute Template说明
GET~/{entityset}/{cast}/$count查询实体集中派生类类型所有Entity的个数
  • Request
curl --location 'http://localhost:5119/odata/Shapes/Lesson2.Models.Rectangle/$count'
  • Response
1

新增(POST)

Request MethodRoute Template说明
POST~/{entityset}向实体集中新增基类类型Entity
POST~/{entityset}/{cast}向实体集中新增派生类类型Entity

在下面接口的代码中,我们使用了Created() 方法。它的作用是用于在 Web API 控制器中返回 HTTP 201 Created 响应,表示资源已被成功创建。它还会在响应体中包含新创建Entity(如 shape、circle、rectangle),并且会自动设置响应头中的 Location,指向新资源的 URI,方便客户端后续访问。

新增基类类型Entity

Request MethodRoute Template说明
POST~/{entityset}向实体集中新增基类类型Entity

添加一个 Shape类对象到集合。需要注意的是:不能通过此新增子类(Circle和Rectangle)对象。

public ActionResult Post([FromBody] Shape shape)
{
    shapes.Add(shape);

    return Created(shape);
}
  • Request
    对基类类型的访问对应的路径是~/{entityset}
curl --location 'http://localhost:5119/odata/Shapes' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 4,
    "Area": 36
}'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes/$entity",
    "Id": 4,
    "Area": 36.0
}

新增派生类类型Entity

Request MethodRoute Template说明
POST~/{entityset}/{cast}向实体集中新增派生类类型Entity

我们有两个子类:Circle和Rectangle。本例我们以Circle为例,添加一个Circle对象。

public ActionResult PostFromCircle([FromBody] Circle circle)
{
    shapes.Add(circle);

    return Created(circle);
}
  • Request
    对派生类类型的访问路径是~/{entityset}/{cast}
curl --location 'http://localhost:5119/odata/Shapes/Lesson2.Models.Circle' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 5,
    "Radius": 1.4,
    "Area": 6.16
}'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Shapes/Lesson2.Models.Circle/$entity",
    "Id": 5,
    "Area": 6.16,
    "Radius": 1.4
}

完整更新(PUT)

OData 标准中,EntitySet(实体集)不支持直接对整个集合使用 PUT 方法。

局部更新(PATCH)

Put和Patch都是更新,但是Put是整体更新,Patch是局部更新。
对于EntitySet,Patch请求会更新其中一些Entity的数据。

Request MethodRoute Template说明
PATCH~/{entityset}局部更新实体集中基类类型Entity
PATCH~/{entityset}/{cast}局部更新实体集中派生类类型Entityy

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

Request MethodRoute Template说明
PATCH~/{entityset}局部更新实体集中基类类型Entity
public ActionResult Patch([FromBody] DeltaSet<Shape> deltaSet)
{
    if (deltaSet == null)
    {
        return BadRequest();
    }
    foreach (Delta<Shape> delta in deltaSet)
    {
        if (delta.TryGetPropertyValue("Id", out object idAsObject))
        {
            var shape = shapes.SingleOrDefault(d => d.Id.Equals(idAsObject));
            if (shape == null)
            {
                return NotFound();
            }
            else if (!shape.GetType().Equals(delta.StructuredType))
            {
                return BadRequest();
            }
            delta.Patch(shape);
        }
    }

    return Ok();
}
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Shapes' \
--header 'Content-Type: application/json' \
--data-raw '{
    "value": [
        {
            "Id": 1,
            "Area": 2
        },
        {
            "@odata.type": "#Lesson2.Models.Circle",
            "Id": 2,
            "Radius": 0.7,
            "Area": 1.54
        },
        {
            "@odata.type": "#Lesson2.Models.Rectangle",
            "Id": 3,
            "Length": 8,
            "Width": 4,
            "Area": 32
        }
    ]
}
'

由于只有Id为1的Entity是基类类型,其对应的数据不用@odata.type描述;其他的非基类类型的Entity都需要声明类型。

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

Request MethodRoute Template说明
PATCH~/{entityset}/{cast}局部更新实体集中派生类类型Entityy
public ActionResult PatchFromRectangle([FromBody] DeltaSet<Rectangle> deltaSet)
{
    if (deltaSet == null)
    {
        return BadRequest();
    }
    foreach (Delta<Rectangle> delta in deltaSet)
    {
        if (delta.TryGetPropertyValue("Id", out object idAsObject))
        {
            var rectangle = shapes.SingleOrDefault(d => d.Id.Equals(idAsObject)) as Rectangle;
            if (rectangle == null) // Ensure rectangle is not null before calling Patch
            {
                return NotFound();
            }
            delta.Patch(rectangle);
        }
    }

    return Ok();
}
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Shapes/Lesson2.Models.Rectangle' \
--header 'Content-Type: application/json' \
--data '{
    "value": [
        {
            "Id": 3,
            "Length": 8,
            "Width": 4,
            "Area": 32
        }
    ]
}'

由于URI路径中已经包含了特定派生类的名称,其和我们要Patch的数据的类型一致,于是负载中就不用增加@odata.type描述。

删除(DELETE)

在 OData 标准中,EntitySet(实体集)不支持直接对整个集合使用 DELETE 方法。

主程序

我们只用将基类类型注册为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、付费专栏及课程。

余额充值