ASP.NET Core OData 实践——Lesson3修改和查询Singleton(C#)


本文是一个基于 ASP.NET Core OData 的 Web API 示例,主要演示了如何使用 OData 单例(Singleton)模式对公司(Company)资源进行管理。OData Singleton(单例实体)本质上代表系统中唯一的、全局存在的对象(如公司、配置等),它在模型定义时就已存在,所以没有“新增”和“删除”接口。

支持的接口

Request MethodRoute Template说明
GET~/{singleton}查询单例对象
GET~/{singleton}/{cast}通过指定派生类型查询单例对象
PUT~/{singleton}更新单例对象
PUT~/{singleton}/{cast}通过指定派生类型更新单例对象
PATCH~/{singleton}局部更新单例对象
PATCH~/{singleton}/{cast}通过指定派生类型局部更新单例对象

主要模型设计

  • Company :整个系统中“公司”实体的基础模型,代表一个通用的公司对象。
  • HoldingCompany :继承自 Company,用于表示控股公司(拥有多个子公司的公司)。NumberOfSubsidiaries是子公司数量,类型为 int,专属于控股公司。

在项目中新建 Models文件夹,并添加Company类和HoldingCompany类。
在这里插入图片描述
对应的文件内容如下

namespace Lesson3.Models
{
    public class Company
    {
        public int Id { get; set; }
        public required string Name { get; set; }
    }
}
namespace Lesson3.Models
{
    public class HoldingCompany : Company
    {
        public int NumberOfSubsidiaries { 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 Lesson3.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace Lesson3.Controllers
{
    public class CompanyController : ODataController
    {
    }
}

下面我们在该类中添加代码逻辑。

数据源

        private static Company company;

        static CompanyController()
        {
            company = new HoldingCompany
            {
                Id = 13,
                Name = "Company LLC",
                NumberOfSubsidiaries = 7
            };
        }

使用静态字段 company 存储唯一的公司对象,初始值为 HoldingCompany(继承自 Company),并设置了相关属性。这样设计保证了全局唯一性,符合 OData 单例的语义。

查询(GET)

Request MethodRoute Template说明
GET~/{singleton}查询单例对象
GET~/{singleton}/{cast}通过指定派生类型查询单例对象

通过基类类型查询

Request MethodRoute Template说明
GET~/{singleton}查询单例对象
        public ActionResult<Company> Get()
        {
            return company;
        }
  • Request
curl --location 'http://localhost:5119/odata/Company'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Company/Lesson3.Models.HoldingCompany",
    "@odata.type": "#Lesson3.Models.HoldingCompany",
    "Id": 13,
    "Name": "Company LLC",
    "NumberOfSubsidiaries": 7
}
  • @odata.context
    指向本次响应对应的元数据文档片段。
    http://localhost:5119/odata/$metadata#Company/Lesson3.Models.HoldingCompany
    表示当前返回的是 Company 单例,实际类型为 Lesson3.Models.HoldingCompany。
  • @odata.type
    #Lesson3.Models.HoldingCompany
    明确指出本次返回的对象实际类型是 HoldingCompany(控股公司),而不是基类 Company。
    只有当实际类型与声明类型不一致(即多态/派生类)时才会出现。

通过派生类类型查询

Request MethodRoute Template说明
GET~/{singleton}/{cast}通过指定派生类型查询单例对象
        public ActionResult<HoldingCompany> GetFromHoldingCompany()
        {
            if (!(company is HoldingCompany holdingCompany))
            {
                return NotFound();
            }

            return holdingCompany;
        }
  • Request
curl --location 'http://localhost:5119/odata/Company/Lesson3.Models.HoldingCompany'
  • Response
{
    "@odata.context": "http://localhost:5119/odata/$metadata#Company/Lesson3.Models.HoldingCompany",
    "Id": 13,
    "Name": "Company LLC",
    "NumberOfSubsidiaries": 7
}

由于URI请求路径中指定的类型和响应的类型一致,所以没有返回@odata.type字段。@odata.context已经明确指出了实际类型是 HoldingCompany。

完整更新(PUT)

Request MethodRoute Template说明
PUT~/{singleton}更新单例对象
PUT~/{singleton}/{cast}通过指定派生类型更新单例对象

通过基类类型完整更新

Request MethodRoute Template说明
PUT~/{singleton}更新单例对象

由于基类类型没有派生类的Property,所以通过基类类型完整更新的只有基类自身的Property。

        public ActionResult Put([FromBody] Company updated)
        {
            if (updated == null)
            {
                return BadRequest("The request body is null or invalid.");
            }
            company.Name = updated.Name;

            return Ok();
        }
  • Request
curl --location --request PUT 'http://localhost:5119/odata/Company' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 13,
    "Name": "Company LLP"
}'

通过派生类类型完整更新

Request MethodRoute Template说明
PUT~/{singleton}/{cast}通过指定派生类型更新单例对象

因为我们的Singleton Entity实际是个派生类型的Entity,所以如果要真正做到完整更新,还是需要通过派生类的PUT方法,这样可以更新所有Property。而通过基类更新只能更新基类有的Property。

        public ActionResult PutFromHoldingCompany([FromBody] HoldingCompany updated)
        {
            if (updated == null)
            {
                return BadRequest("The request body is null or invalid.");
            }

            if (!(company is HoldingCompany holdingCompany))
            {
                return NotFound();
            }

            holdingCompany.Name = updated.Name;
            holdingCompany.NumberOfSubsidiaries = updated.NumberOfSubsidiaries;

            return Ok();
        }
  • Request
curl --location --request PUT 'http://localhost:5119/odata/Company/Lesson3.Models.HoldingCompany' \
--header 'Content-Type: application/json' \
--data '{
    "Id": 13,
    "Name": "Company LTD",
    "NumberOfSubsidiaries": 5
}'

局部更新(PATCH)

Request MethodRoute Template说明
PATCH~/{singleton}局部更新单例对象
PATCH~/{singleton}/{cast}通过指定派生类型局部更新单例对象

通过基类类型局部更新

Request MethodRoute Template说明
PATCH~/{singleton}局部更新单例对象

通过基类进行局部更新也只能更新基类所拥有的Property。

        public ActionResult Patch([FromBody] Delta<Company> delta)
        {
            if (delta == null)
            {
                return BadRequest("The request body is null or invalid.");
            }
            delta.Patch(company);

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

通过派生类类型局部更新

Request MethodRoute Template说明
PATCH~/{singleton}/{cast}通过指定派生类型局部更新单例对象
        public ActionResult PatchFromHoldingCompany([FromBody] Delta<HoldingCompany> delta)
        {
            if (delta == null)
            {
                return BadRequest("The request body is null or invalid.");
            }

            if (!(company is HoldingCompany holdingCompany))
            {
                return NotFound();
            }

            delta.Patch(holdingCompany);

            return Ok();
        }
  • Request
curl --location --request PATCH 'http://localhost:5119/odata/Company/Lesson3.Models.HoldingCompany' \
--header 'Content-Type: application/json' \
--data '{
    "Name": "Company A/S",
    "NumberOfSubsidiaries": 3
}'

主程序

不同于之前介绍的EntitySet的注册,我们注册Singleton需要使用Singleton()方法。

using Lesson3.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.Singleton<Company>("Company");
    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

可以看到kind是Singleton

{
    "@odata.context": "http://localhost:5119/odata/$metadata",
    "value": [
        {
            "name": "Company",
            "kind": "Singleton",
            "url": "Company"
        }
    ]
}

模型元文档

  • 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="Lesson3.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Company">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Name" Type="Edm.String" Nullable="false" />
            </EntityType>
            <EntityType Name="HoldingCompany" BaseType="Lesson3.Models.Company">
                <Property Name="NumberOfSubsidiaries" Type="Edm.Int32" Nullable="false" />
            </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <Singleton Name="Company" Type="Lesson3.Models.Company" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

它包含如下几部分:

  1. 实体类型定义
  • Company
    • EntityType Name=“Company”:定义了一个名为 Company 的实体类型。
    • <Key><PropertyRef Name=“Id” /></Key>:以 Id 属性作为主键。
    • <Property Name=“Id” Type=“Edm.Int32” Nullable=“false” />:Id 是 int 类型,不能为空。
    • <Property Name=“Name” Type=“Edm.String” Nullable=“false” />:Name 是字符串类型,不能为空。
  • HoldingCompany
    • EntityType Name=“HoldingCompany” BaseType=“Lesson3.Models.Company”:定义了一个名为 HoldingCompany 的实体类型,继承自 Company。
    • <Property Name=“NumberOfSubsidiaries” Type=“Edm.Int32” Nullable=“false” />:新增属性 NumberOfSubsidiaries,表示子公司数量,int 类型,不能为空。
  1. 实体容器与单例
  • EntityContainer Name=“Container”:定义了一个实体容器,OData 服务的入口。
  • <Singleton Name=“Company” Type=“Lesson3.Models.Company” />:定义了一个名为 Company 的单例(Singleton),类型为 Company。
    这意味着 OData 服务中只有一个全局唯一的 Company 实例(可为 Company 或其派生类,如 HoldingCompany)。
  1. 继承关系
  • HoldingCompany 通过 BaseType 继承自 Company,拥有 Company 的所有属性,并扩展了自己的属性。

代码地址

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

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

breaksoftware

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

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

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

打赏作者

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

抵扣说明:

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

余额充值