ASP.NET Core OData 实践——Lesson2增删改查Entity和EntitySet(C#)
大纲
- 支持的接口
- Entity
- EntitySet
- 主要模型设计(Models)
- 控制器设计(ShapesController)
- 数据源
- 新增Entity(POST)
- 新增基类类型Entity
- 新增特定派生类类型Entity
- 查询EntitySet和Entity(GET)
- 查询EntitySet
- 查询所有基类类型以及其派生类类型的EntitySet
- 查询特定派生类类型的EntitySet
- 查询所有基类类型以及其派生类类型的Entity的个数
- 查询特定派生类类型的Entity的个数
- 查询Entity
- 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象
- 返回数据类型和请求类型一致
- 返回数据类型和请求类型不一致
- 查询实体集中特定派生类类型的对象中指定Key的对象
- 完整更新Entity(PUT)
- 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象
- Entity类型和请求路径中类型一致
- Entity类型和请求路径中类型不一致
- 更新实体集中特定派生类类型的对象中指定Key的对象
- 局部更新EntitySet和Entity(PATCH)
- 更新Entity中部分Property
- 通过基类类型进行局部更新
- 通过指定派生类类型进行局部更新
- 更新EntitySet中部分Entity
- 通过基类类型进行局部更新
- 通过指定派生类类型进行局部更新
- 删除Entity(DELETE)
- 通过基类类型删除对象
- 通过特定派生类类型删除对象
- 主程序
- 服务文档
- 模型元文档
- 代码地址
- 参考资料
本文是一个基于 ASP.NET Core OData 的 Web API 示例,主要演示了如何通过 OData 协议对多态形状(Shape、Circle、Rectangle)进行增删改查(CRUD)操作。项目采用内存集合模拟数据存储,便于理解 OData 控制器的基本用法和多态数据的处理方式。
支持的接口
Entity
Entity需要存在于EntitySet之中,所以新增Entity是要在EntitySet的接口中。
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key} | 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}({key}) | 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}/{key}/{cast} | 查询实体集中特定派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}({key})/{cast} | 查询实体集中特定派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}/{key} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}({key}) | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}/{key}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}({key})/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象 |
PATCH | ~/{entityset}/{key} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}({key}) | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}/{key}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}({key})/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分Property |
DELETE | ~/{entityset}/{key} | 删除实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
DELETE | ~/{entityset}({key}) | 删除实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
DELETE | ~/{entityset}/{key}/{cast} | 删除实体集中特定派生类类型的对象中指定Key的对象 |
DELETE | ~/{entityset}({key})/{cast} | 删除实体集中特定派生类类型的对象中指定Key的对象 |
EntitySet
EntitySet自身不可以新增,只能向其新增Entity。
由于Put是完整更新,所以EntitySet没有该接口。但是可以更新部分的Entity(局部修改),所以可以有Patch接口。
同时EntitySet自身也不能删除,所以也没有DELETE接口。
Request Method | Route Template | 说明 |
---|---|---|
POST | ~/{entityset} | 用于向实体集中基类类型添加对象 |
POST | ~/{entityset}/{cast} | 用于向实体集中特定派生类型添加对象 |
GET | ~/{entityset} | 查询实体集中基类类型以及其派生类类型的对象 |
GET | ~/{entityset}/$count | 查询实体集中基类类型以及其派生类类型的对象的个数 |
GET | ~/{entityset}/{cast} | 查询实体集中特定派生类类型的对象 |
GET | ~/{entityset}/{cast}/$count | 查询实体集中特定派生类类型的对象的个数 |
PATCH | ~/{entityset} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分Property |
主要模型设计(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,以便拥有如下能力:
- OData 路由支持
继承 ODataController 后,控制器自动支持 OData 路由(如 /odata/Shapes(1)),可以直接响应 OData 标准的 URL 路径和操作。 - OData 查询参数支持
可以使用 [EnableQuery] 特性,自动支持 $filter、$select、$orderby、$expand 等 OData 查询参数,无需手动解析。 - OData 响应格式
返回的数据会自动序列化为 OData 标准格式(如 JSON OData),方便前端或其他系统消费。 - OData Delta 支持
支持 Delta<T>、DeltaSet<T> 等类型,便于实现 PATCH、批量 PATCH 等 OData 特有的部分更新操作。 - 更丰富的 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 存储所有形状对象,便于演示和测试。
新增Entity(POST)
Request Method | Route Template | 说明 |
---|---|---|
POST | ~/{entityset} | 用于向实体集中基类类型添加对象 |
POST | ~/{entityset}/{cast} | 用于向实体集中特定派生类型添加对象 |
在下面接口的代码中,我们使用了Created() 方法。它的作用是用于在 Web API 控制器中返回 HTTP 201 Created 响应,表示资源已被成功创建。它还会再响应体中包含新创建的对象(如 shape、circle、rectangle),并且会自动设置响应头中的 Location,指向新资源的 URI,方便客户端后续访问。
新增基类类型Entity
Request Method | Route Template | 说明 |
---|---|---|
POST | ~/{entityset} | 用于向实体集中基类类型添加对象 |
添加一个 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 Method | Route Template | 说明 |
---|---|---|
POST | ~/{entityset}/{cast} | 用于向实体集中特定派生类型添加对象 |
我们有两个子类: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
}
查询EntitySet和Entity(GET)
查询EntitySet
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset} | 查询实体集中基类类型以及其派生类类型的对象 |
GET | ~/{entityset}/$count | 查询实体集中基类类型以及其派生类类型的对象的个数 |
GET | ~/{entityset}/{cast} | 查询实体集中特定派生类类型的对象 |
GET | ~/{entityset}/{cast}/$count | 查询实体集中特定派生类类型的对象的个数 |
在下面的接口代码中,我们使用了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
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset} | 查询实体集中基类类型以及其派生类类型的对象 |
[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
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{cast} | 查询实体集中特定派生类类型的对象 |
以查询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}]
}
查询所有基类类型以及其派生类类型的Entity的个数
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/$count | 查询实体集中基类类型以及其派生类类型的对象的个数 |
- Request
curl --location 'http://localhost:5119/odata/Shapes/$count'
- Response
3
查询特定派生类类型的Entity的个数
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{cast}/$count | 查询实体集中特定派生类类型的对象的个数 |
- Request
curl --location 'http://localhost:5119/odata/Shapes/Lesson2.Models.Rectangle/$count'
- Response
1
查询Entity
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key} | 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}({key}) | 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}/{key}/{cast} | 查询实体集中特定派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}({key})/{cast} | 查询实体集中特定派生类类型的对象中指定Key的对象 |
查询实体集中基类类型以及其派生类类型的对象中指定Key的对象
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key} | 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}({key}) | 查询实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
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的对象
Request Method | Route Template | 说明 |
---|---|---|
GET | ~/{entityset}/{key}/{cast} | 查询实体集中特定派生类类型的对象中指定Key的对象 |
GET | ~/{entityset}({key})/{cast} | 查询实体集中特定派生类类型的对象中指定Key的对象 |
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 Method | Route Template | 说明 |
---|---|---|
PUT | ~/{entityset}/{key} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}({key}) | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}/{key}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}({key})/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象 |
• 语义:整体替换。
• 用法:客户端需要提交整个对象的所有属性(即使只改了一个字段,也要把所有字段都发过来)。
• 行为:服务器用客户端提交的对象完全替换现有对象,未提交的字段会被重置为默认值。
更新实体集中基类类型以及其派生类类型的对象中指定Key的对象
Request Method | Route Template | 说明 |
---|---|---|
PUT | ~/{entityset}/{key} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}({key}) | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
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 reflectionforeach (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 CanWriteif (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的对象
Request Method | Route Template | 说明 |
---|---|---|
PUT | ~/{entityset}/{key}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象 |
PUT | ~/{entityset}({key})/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象 |
由于已经指定了特定派生类,这样我们就不需要像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
}'
局部更新EntitySet和Entity(PATCH)
Put和Patch都是更新,但是Put是整体更新,Patch是局部更新。
对于Entity,Patch请求只需要发送需要更改的Property的内容,其他Property的值保持不变。
对于EntitySet,Patch请求会更新其中一些Entity的数据。
更新Entity中部分Property
Request Method | Route Template | 说明 |
---|---|---|
PATCH | ~/{entityset}/{key} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}({key}) | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}/{key}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}({key})/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分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
来声明数据的类型。
更新EntitySet中部分Entity
Request Method | Route Template | 说明 |
---|---|---|
PATCH | ~/{entityset} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
PATCH | ~/{entityset}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分Property |
通过基类类型进行局部更新
Request Method | Route Template | 说明 |
---|---|---|
PATCH | ~/{entityset} | 更新实体集中基类类型以及其派生类类型的对象中指定Key的对象的部分Property |
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 Method | Route Template | 说明 |
---|---|---|
PATCH | ~/{entityset}/{cast} | 更新实体集中特定派生类类型的对象中指定Key的对象的部分Property |
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
描述。
删除Entity(DELETE)
Request Method | Route Template | 说明 |
---|---|---|
DELETE | ~/{entityset}/{key} | 删除实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
DELETE | ~/{entityset}({key}) | 删除实体集中基类类型以及其派生类类型的对象中指定Key的对象 |
DELETE | ~/{entityset}/{key}/{cast} | 删除实体集中特定派生类类型的对象中指定Key的对象 |
DELETE | ~/{entityset}({key})/{cast} | 删除实体集中特定派生类类型的对象中指定Key的对象 |
删除数据需要指定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>
它包含如下几部分:
- 实体类型定义
- 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 类型,不能为空。
- 实体容器与实体集
- EntityContainer Name=“Container”:定义了一个实体容器,OData 服务的入口。
- <EntitySet Name=“Shapes” EntityType=“Lesson2.Models.Shape” />:定义了一个名为 Shapes 的实体集,类型为 Shape。
这意味着可以通过 /odata/Shapes 路由访问所有 Shape 及其派生类(Circle、Rectangle)对象。
- 继承关系
- Circle 和 Rectangle 都通过 BaseType 继承自 Shape,拥有 Shape 的所有属性,并扩展了自己的属性。
代码地址
https://github.com/f304646673/odata/tree/main/csharp/Lesson/Lesson2
参考资料
- https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/entity-routing?tabs=net60%2Cvisual-studio