ASP.NET Core OData 实践——Lesson9绑定和未绑定的Function和Action(C#)

article/2025/6/27 4:59:17

大纲

  • 概念
  • 支持的接口
  • 主要模型设计
  • 控制器设计
    • 数据源
    • Function
      • Bound Function
      • Unbound Function
      • 重载(overload)
    • Action
      • Bound Action
      • Unbound Action
      • 重载(overload)
        • Bound Action
        • Unbound Action
  • 主程序
    • 服务文档
    • 模型元文档
  • 代码地址
  • 参考资料

在构建 OData 服务时,Actions 和 Functions 是扩展服务器端行为的核心机制,尤其适用于无法通过传统 CRUD(创建、读取、更新、删除)操作实现的业务逻辑。本文基于 ASP.NET Core OData v8.x,深入解析两者的差异,并通过实战示例演示如何在 OData v4 端点中使用它们。

概念

Function 是 OData 服务中暴露的一种 幂等性操作,用于封装 查询计算转换逻辑
Action 是 OData 服务中暴露的一种 非幂等性操作,用于封装命令式逻辑

它们的区别如下表所示

特性Functions(函数)Actions(动作)
本质幂等的查询 / 计算操作(无副作用)非幂等的命令操作(可修改服务器状态)
入参基本类型(int、string)复杂类型(ComplexType/Entity)
返回值必须返回数据(实体、集合或基本类型)可选返回值(成功响应可能无内容)
HTTP 方法GET(强制要求,符合查询语义)POST(唯一合法方法,确保状态变更安全性)
组合能力支持与 $filter、$expand 等查询参数组合不支持组合(避免因多次调用导致不确定结果)
典型场景数据查询、动态计算(如统计销售额、生成折扣价)状态变更、复杂业务逻辑(如提交订单、审批流程)

核心区别可概括为:

  • Functions 是 “只读的计算器”,专注于数据查询与计算;
  • Actions 是 “状态的改变者”,负责执行命令与修改资源。

在下面的项目中,我们对其进行实践。

支持的接口

Request MethodRoute Template说明
GET~/{entityset}|{singleton}/{function}调用绑定到EntitySet或者Singleton的Function
GET~/{entityset}|{singleton}/{cast}/{function}调用绑定到EntitySet或者Singleton的特定派生类型的Function
GET~/{entityset}/{key}/{function}通过指定Key的Entity使用Function
GET~/{entityset}/{key}/{cast}/{function}通过指定Key的特定派生类型Entity使用Function
GET~/{function}(param1={value},param2={value})调用非绑定Function
POST~/{entityset}|{singleton}/{action}调用绑定到EntitySet或者Singleton的Action
POST~/{entityset}|{singleton}/{cast}/{action}调用绑定到EntitySet或者Singleton的特定派生类型的Action
POST~/{entityset}/{key}/{action}通过指定Key调用绑定到Entity的Action
POST~/{entityset}/{key}/{cast}/{action}通过指定Keyd调用绑定到特定派生类型的Action
POST~/{action}调用非绑定Action

主要模型设计

在项目下新增Models文件夹,并添加Book和BookRating类。
在这里插入图片描述
Book 类用于表示一本图书的数据结构。其Property包含:

  • Id:图书唯一标识,类型为 string,必须赋值(required)。
  • Title:图书标题,类型为 string,必须赋值。
  • Author:作者,类型为 string,必须赋值。
  • ForKids:是否为儿童书籍,类型为 bool,必须赋值。
  • Year:出版年份,类型为可空整型(int?),默认值为 0。可空表示该字段可以不赋值。
namespace Lesson9.Models
{public class Book{public required string Id { get; set; }public required string Title { get; set; }public required string Author { get; set; }public required bool ForKids { get; set; }public int? Year { get; set; } = 0;}
}

BookRating 类用于表示一本书的评分信息。其Property包含:

  • Id:评分的唯一标识,类型为可空字符串(string?)。可以为空,通常用于数据库主键或唯一标识。
  • Rating:评分值,类型为 int。用于记录对图书的评分分数。
  • BookID:被评分图书的唯一标识,类型为 string,并且是 required,即对象初始化时必须赋值。用于关联到具体的 Book。
namespace Lesson9.Models
{public class BookRating{public string? Id { get; set; }public int Rating { get; set; }public required string BookID { get; set; }}
}

控制器设计

在这里插入图片描述

在项目中新增Controller文件夹,然后BooksController类。该类注册于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 Lesson9.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;namespace Lesson9.Controllers
{public class BooksController: ODataController{}
}

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

数据源

定义了一个静态的 Book 类型列表,命名为 books,用于存储所有图书数据。

        private static List<Book> books = [new Book { Id = "1", Title = "Book 1", Author = "Author 1", ForKids = true },new Book { Id = "2", Title = "Book 2", Author = "Author 2", ForKids = false },new Book { Id = "3", Title = "Book 3", Author = "Author 3", ForKids = true },new Book { Id = "4", Title = "Book 4", Author = "Author 4", ForKids = false }];

Function

我们先定义相应的Function逻辑。

Bound Function

        [HttpGet("odata/Books/mostRecent()")]public IActionResult MostRecent(){var maxBookId = books.Max(x => x.Id);return Ok(maxBookId);}

Function可以bound到Entity、EntitySet和SIngleton上。本例中,我们通过URI可以看到MostRecent映射到Books这个EntitySet上了。这也预示着,该方法只能通过EntitySet来访问,所以它不用暴露在服务文档中。对它的调用是需要先在服务文档中发现Books这个EntitySet,然后在元数据文档中查询到该EntitySet下绑定了哪些Function
需要注意的是,此处只是声明其实现,并设置解释URI,实际的Bound操作是在主函数中设置的。

Unbound Function

        [HttpGet("odata/ReturnAllForKidsBooks")]public IActionResult ReturnAllForKidsBooks(){var forKidsBooks = books.Where(m => m.ForKids == true);return Ok(forKidsBooks);}

我们通过URI可以看到ReturnAllForKidsBooks并没有绑定任何类型。而设置该Function是否绑定则是在主函数中。

重载(overload)

在 OData 中,Function 支持重载(Overloading),但需遵循严格的规范和约束。

  1. 参数签名必须不同
    同一 Function 名称下的不同重载版本,必须通过参数数量类型顺序区分。
<Function Name="GetProducts" IsBound="false"><ReturnType Type="Collection(ns.Product)" />
</Function><Function Name="GetProducts" IsBound="false"><Parameter Name="category" Type="Edm.String" /><ReturnType Type="Collection(ns.Product)" />
</Function>

两个 GetProducts 函数通过是否包含 category 参数区分。
2. 绑定类型可不同
同一 Function 名称可绑定到不同类型(如Entity、EntitySet或者SIngleton)

<Function Name="GetDemo" IsBound="true"><Parameter Name="bindingParameter" Type="Collection(Lesson9.Models.Book)" /><ReturnType Type="Edm.String" />
</Function>
<Function Name="GetDemo" IsBound="true"><Parameter Name="bindingParameter" Type="Lesson9.Models.Book" /><ReturnType Type="Edm.String" />
</Function>
  1. 返回类型不可单独作为区分依据
    仅返回类型不同的重载会导致元数据冲突,OData 处理器无法根据请求路径确定具体调用哪个版本。
<!-- 错误:仅返回类型不同,无法重载 -->
<Function Name="GetData"><ReturnType Type="Edm.String" />
</Function><Function Name="GetData"><ReturnType Type="Edm.Int32" />
</Function>

Action

Bound Action

        [HttpPost("odata/Books({key})/Rate")]public IActionResult Rate([FromODataUri] string key, ODataActionParameters parameters){if (!ModelState.IsValid){return BadRequest();}int rating = (int)parameters["rating"];if (rating < 0){return BadRequest();}return Ok(new BookRating() { BookID = key, Rating = rating });}

Action也可以绑定到Entity、EntitySet和SIngleton。通过URI我们可以发现,Rate被绑定到Book这个Entity上,所以我们要访问该Action就需要通过Book Entity资源来定位。

Unbound Action

        [HttpPost("odata/incrementBookYear")]public IActionResult IncrementBookYear(ODataActionParameters parameters){if (!ModelState.IsValid){return BadRequest();}int increment = (int)parameters["increment"];string bookId = (string)parameters["id"];var book = books.Where(m => m.Id == bookId).FirstOrDefault();if (book != null){book.Year = book.Year + increment;}return Ok(book);}

Unbound Action的URI上是没有资源名称的,但是它并不能直接在服务文档上暴露出来。这是因为Action是有状态插座,而服务文档上默认暴露的是无状态操作。

重载(overload)

Action 支持重载(Overloading),但需遵循严格的规范和约束。

Bound Action
  1. 仅允许通过绑定类型区分重载
    同名 Action 必须绑定到 不同的目标类型(Entity、EntitySet或者SIngleton)。
 <Action Name="rate" IsBound="true"><Parameter Name="bindingParameter" Type="Lesson9.Models.Book" /><Parameter Name="rating" Type="Edm.Int32" Nullable="false" /></Action><Action Name="rate" IsBound="true"><Parameter Name="bindingParameter" Type="Collection(Lesson9.Models.Book)" /><Parameter Name="rating" Type="Edm.Int32" Nullable="false" /></Action>

这是因为在访问时,服务可以通过不同URI来进行区分:

  • POST odata/Books(1)/rate
  • POST odata/Books/rate
  1. 禁止同一绑定类型下的参数重载
    若绑定类型相同(如均为 ns.Order),即使参数列表不同,也不允许重载。
<!-- 非法:绑定类型均为 Lesson9.Models.Book,参数不同但无法重载 -->
<Action Name="rate" IsBound="true"><Parameter Name="bindingParameter" Type="Lesson9.Models.Book" /><Parameter Name="rating" Type="Edm.Int32" Nullable="false" />
</Action>
<Action Name="rate" IsBound="true"><Parameter Name="bindingParameter" Type="Lesson9.Models.Book" /><Parameter Name="rating" Type="Edm.String" Nullable="false" />
</Action>
Unbound Action

不支持重载。

主程序

using Lesson9.Models;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using System;var builder = WebApplication.CreateBuilder(args);// 提取 OData EDM 模型构建为方法,便于维护和扩展
static IEdmModel GetEdmModel()
{var modelBuilder = new ODataConventionModelBuilder();modelBuilder.EntitySet<Book>("books");modelBuilder.EntityType<Book>().Collection.Function("mostRecent").Returns<string>();modelBuilder.Function("returnAllForKidsBooks").ReturnsFromEntitySet<Book>("books");modelBuilder.EntityType<Book>().Action("rate").Parameter<int>("rating");var action = modelBuilder.Action("incrementBookYear").ReturnsFromEntitySet<Book>("books");action.Parameter<int>("increment");action.Parameter<string>("id");return modelBuilder.GetEdmModel();
}// 添加 OData 服务和配置
builder.Services.AddControllers().AddOData(options =>{options.AddRouteComponents("odata", GetEdmModel()).Count().OrderBy().Filter().Select().Expand();options.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;});var app = builder.Build();app.UseRouting();app.MapControllers();app.Run();

这段代码将Function mostRecent绑定到Book的Collection上,即Books这个EntitySet;将Action rate绑定到Book这个Entity上。returnAllForKidsBook是Unbound Function;incrementBookYear是Unbound Action。
options.RouteOptions.EnableNonParenthesisForEmptyParameterFunction的设置是针对没有入参的 mostRecent。这样我们就可以通过GET odata/Books/mostRecent请求到数据,而不用像GET odata/Books/mostRecent()这样一定要加上括号。

服务文档

  • Request
curl --location 'http://localhost:5119/odata'
  • Response
{"@odata.context": "http://localhost:5119/odata/$metadata","value": [{"name": "books","kind": "EntitySet","url": "books"},{"name": "returnAllForKidsBooks","kind": "FunctionImport","url": "returnAllForKidsBooks"}]
}

在本案例中,我们定义了一个Unbound Function和一个Unbound Action。但是只有Unbound Function以FunctionImport形式在服务文档中可见,但是ActionImport并不可见。
这是因为服务文档主要是以 JSON 或 Atom 格式描述服务的可寻址资源可调用的无状态操作。而Action是有状态的操作,所以并不会默认出现在服务文档中。

模型元文档

  • 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="Lesson9.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm"><EntityType Name="Book"><Key><PropertyRef Name="Id" /></Key><Property Name="Id" Type="Edm.String" Nullable="false" /><Property Name="Title" Type="Edm.String" Nullable="false" /><Property Name="Author" Type="Edm.String" Nullable="false" /><Property Name="ForKids" Type="Edm.Boolean" Nullable="false" /><Property Name="Year" Type="Edm.Int32" /></EntityType></Schema><Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm"><Function Name="mostRecent" IsBound="true"><Parameter Name="bindingParameter" Type="Collection(Lesson9.Models.Book)" /><ReturnType Type="Edm.String" /></Function><Function Name="returnAllForKidsBooks"><ReturnType Type="Lesson9.Models.Book" /></Function><Action Name="rate" IsBound="true"><Parameter Name="bindingParameter" Type="Lesson9.Models.Book" /><Parameter Name="rating" Type="Edm.Int32" Nullable="false" /></Action><Action Name="incrementBookYear"><Parameter Name="increment" Type="Edm.Int32" Nullable="false" /><Parameter Name="id" Type="Edm.String" /><ReturnType Type="Lesson9.Models.Book" /></Action><EntityContainer Name="Container"><EntitySet Name="books" EntityType="Lesson9.Models.Book" /><FunctionImport Name="returnAllForKidsBooks" Function="Default.returnAllForKidsBooks" EntitySet="books" IncludeInServiceDocument="true" /><ActionImport Name="incrementBookYear" Action="Default.incrementBookYear" EntitySet="books" /></EntityContainer></Schema></edmx:DataServices>
</edmx:Edmx>

mostRecent是Bound到EntitySet(Collection of Books )上的Function,所以IsBound="true",同时第一个参数命名为Name="bindingParameter"。(\<Parameter Name="bindingParameter" Type="Collection(Lesson9.Models.Book)" /\>).
returnAllForKidsBooks是Unbound Function。在Container里,它被通过FunctionImport 描述,同时IncludeInServiceDocument="true"表示它会出现在服务文档中。
rate是Bound到Entity(Book)上的Action,所以IsBound="true",同时第一个参数命名为Name="bindingParameter"。(\<Parameter Name="bindingParameter" Type="Lesson9.Models.Book" /\>)
incrementBookYear是Unbound Action。在Container里,它被通过ActionImport 描述。由于Action是有状态的操作,所以它不会出现在服务文档中,即IncludeInServiceDocument取了默认值false。

代码地址

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

参考资料

  • https://learn.microsoft.com/en-us/odata/webapi-8/fundamentals/actions-functions

http://www.hkcw.cn/article/UQYaLIEmIX.shtml

相关文章

描述性统计——让数据说话

第03篇&#xff1a;描述性统计——让数据说话 写在前面&#xff1a;大家好&#xff0c;我是蓝皮怪&#xff01;前两篇我们聊了统计学的基本概念和数据类型&#xff0c;这一篇我们要正式进入数据分析的第一步——描述性统计。别被名字吓到&#xff0c;其实就是用一组数字&#x…

【MySQL基础】库的操作:创建、删除与管理数据库

MySQL学习&#xff1a; https://blog.csdn.net/2301_80220607/category_12971838.html?spm1001.2014.3001.5482 前言&#xff1a; 在上一篇我们已经讲解了数据库的基本内容&#xff0c;相信大家对数据库已经有了一些自己的理解&#xff0c;从这篇开始我们就开始正式进入如何…

国足抵达雅加达备战世预赛 力争两连胜晋级希望

中国男足国家队于6月2日晚抵达印度尼西亚首都雅加达,准备参加5日举行的2026美加墨世界杯亚洲区预选赛18强赛第9轮对阵印尼队的比赛。当地时间晚上10时30分,中国队在主教练伊万科维奇的带领下走出雅加达苏加诺-哈达国际机场,随后乘坐大巴前往酒店。伊万科维奇表示,中国队在…

中国龙舟文化“划”向全世界

央视网消息:这个端午假期,热气腾腾的“端午经济”成为消费活力升级的缩影。“国潮”风引领文化消费新风尚,传统文化元素与现代技术交融,非遗体验“烟火气”满满,打造出独特的“国潮端午”氛围,持续火热的国潮消费也一路“火”到了海外。这段时间,在义乌国际商贸城做3D打…

马斯克宣布离职:不想为政府政策负责 “政府效率部”成替罪羊

埃隆马斯克在接受美国哥伦比亚广播公司采访时提到,他并不想公开反对美国政府,但也不愿意为政府所做的一切承担责任。他表示,他领导的“政府效率部”成了所有问题的替罪羊,无论是真是假的裁员都归咎于这个部门。马斯克还表示,他对国会共和党正在讨论的数万亿美元减税与支出…

韩国5名候选人竞逐总统 李在明领跑民调

韩国第21届总统大选于当地时间6月3日6时正式开始,全国共设有14295个投票站。没有参加提前投票的选民凭本人身份证件前往指定投票站即可参加投票,投票将于当日20时结束。本次大选共有7位候选人进行了登记,但其中两位先后宣布退出,并表示支持国民力量党候选人金文洙。因此,选…

学者:李在明若胜将大幅调整外交政策 韩国大选临近决策点

韩国总统大选即将于3日迎来正式投票。根据选前多项民调结果,共同民主党候选人李在明仍以明显优势领先国民力量党的金文洙和改革新党的李俊锡。金文洙与李俊锡合并无望的情况下,李在明距离总统宝座仅一步之遥。2日举行的选前最后一场记者会几乎成为了李在明的“总统政策说明会…

2025/6月最新Cursor(0.50.5版本)一键自动更换邮箱无限续杯教程

使用前检查&#xff1a; 使用前请先看左下角&#xff0c;是否获取到Cursor的版本号 如果没有请先在 功能页面 -→ 自定义Cursor路径 选择你Cursor的安装的路径&#xff0c;并开启后重启YCursor&#xff0c;获取到版本后才能正常使用功能 检查软件左下角的权限标识是否为绿色 如…

算法:二分查找

1.二分查找 704. 二分查找 - 力扣&#xff08;LeetCode&#xff09; 二分查找算法要确定“二段性”&#xff0c;时间复杂度为O(lonN)。为了防止数据溢出&#xff0c;所以求mid时要用防溢出的方式。 class Solution { public:int search(vector<int>& nums, int tar…

Elasticsearch 读写流程深度解析

在数据驱动的数字化浪潮中&#xff0c;Elasticsearch 凭借其毫秒级搜索响应与水平扩展能力&#xff0c;已成为现代数据架构的核心引擎。本文将深入剖析其读写流程的设计思想、实现细节与工程权衡&#xff0c;揭示这一分布式系统的精妙架构。 一、 架构基石&#xff1a;分布式设…

2024年第十五届蓝桥杯Scratch10月stema选拔赛真题——数字卡片排序

2024年第十五届蓝桥杯Scratch10月stema选拔赛真题——数字卡片排序 题目可点下面去处&#xff0c;支持在线编程~ 数字卡片排序_scratch_少儿编程题库学习中心-嗨信奥 程序演示可下下面去处&#xff0c;支持获取素材和源码~ 数字卡片排序-scratch作品-少儿编程题库学习中心-嗨…

基于遥感图像深度学习的海洋测深

知识星球&#xff1a;数据书局。打算通过知识星球将这些年积累的知识、经验分享出来&#xff0c;让各位在数据治理、数据分析的路上少走弯路&#xff0c;另外星球也方便动态更新最近的资料&#xff0c;提供各位一起讨论数据的小圈子 1. 摘要 沿海开发和规划面临的问题&#…

《使命召唤》防线失守:系列多款游戏被破解,黑客公开源代码 堡垒首次被突破

每当一款新游戏在PC平台发售,如果未使用Denuvo加密技术,破解者们就会竞相争夺首个破解该作品的机会。例如,《漫威蜘蛛侠 2》和《最后生还者 2》分别在发售后不到两分钟和一天内被破解。长期以来,《使命召唤》系列因其独特的数字版权管理技术和始终在线的网络连接而被视为难…

男子端午节爬野山迷路,还执意自己找路!27人冒雨搜山救援 公益救援彰显大爱

5月31日端午节,在北京房山的一处野山中,一名男子登山迷路却不想麻烦救援队,坚持要自己摸索下山。男子曾向警方询问下山道路,但拒绝了蓝天救援队的帮助。然而不久后,他再次联系救援队请求援助,称自己过于自信,但找不到路。尽管被困男子最初未请求救援,房山蓝天救援队出于…

中山漫展 女童暴露服装引争议

中山漫展 女童暴露服装引争议!6月1日,在广东中山漫展现场,观众看到两名女童身着暴露服装参加付费直播活动,纷纷提出质疑。微信公众号“中山博览中心”5月27日发文称,5月31日至6月1日10点-17点,将在中山博览中心前厅和综合展厅举行“2025中山AS24端午动漫嘉年华”活动。文…

前端八股之CSS

CSS 盒子模型深度解析与实战 一、盒子模型核心概念 Box-sizing CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度 语法&#xff1a; box-sizing: content-box|border-box|inherit:content-box 默认值&#xff0c;元素的 width/height 不包含paddi…

渊龙靶场-sql注入(数字型注入)

1.开局请求抓包 测试点如上图&#xff0c;测试注入&#xff0c;存在注入。 2.查询列数 我们再查他多少列 ,最后测试为为2列。 3.查询回显位 发现均可以回显 4.查询表 插入语句查询表和数据库 union select database(),group_concat(table_name) FROM information_schema.t…

Linux内核体系结构简析

1.Linux内核 1.1 Linux内核的任务 从技术层面讲&#xff0c;内核是硬件和软件之间的一个中间层&#xff0c;作用是将应用层序的请求传递给硬件&#xff0c;并充当底层驱动程序&#xff0c;对系统中的各种设备和组件进行寻址。从应用程序的角度讲&#xff0c;应用程序与硬件没有…

ESP-IDF 离线安装——同时存在多个版本以及进行版本切换的方法

一、离线安装包的下载方法 ESP-IDF离线安装包下载链接 我下载了下面三个版本进行测试 二、离线安装包的安装方法 1.创建文件夹 创建ESP-IDF文件夹&#xff0c;并为不同版本的IDF分别创建一个文件夹&#xff0c;如下图所示 2.双击离线安装包&#xff08;以5.0版本为例&am…

企业数实产业技术融合数据(2000-2024)

1943 企业数实产业技术融合(2000-2024&#xff09; 数据简介 当前&#xff0c;高质量发展成为经济发展主赛道&#xff0c;新质生产力不仅是经济转型的关键力量 ,更是引领新兴战略性产业、提高国家竞争力的核心要素。在全球经济动荡格局中&#xff0c;发展新质生产力对推动高…