☰
Microsoft Graph 指南:用 Filter-as-Segment 与 Filter 函数对集合子集执行批量操作
2026/10/1 7:28:57 网站建设 项目流程
  • API设计

【免费下载链接】api-guidelines

Microsoft REST API Guidelines

项目地址:https://gitcode.com/gh_mirrors/ap/api-guidelines
点击查看免费下载

在 Microsoft Graph 的 API 设计中,经常会遇到"对集合中满足某一条件的子集执行一个操作"的场景——例如批量"撤销风险用户"(dismiss risky users)。OData V4.01 提供了一个名为 filter-as-segment 的 URL 特性,允许把$filter表达式直接放进 URL 路径段;而 Microsoft Graph 指南在此基础上给出了一个更推荐的做法:引入一个绑定在集合上的可组合filter函数,把过滤表达式作为字符串参数传入,从而在保留 OData 过滤表达式强大表达能力的同时,规避 filter-as-segment 在可发现性与参数别名支持上的顾虑。读完本文,你将掌握这两种写法的完整 CSDL 建模、对应 HTTP 调用方式、单引号转义规则,以及它们与仓库中 operations、collections 等既有指南的衔接关系。

场景起点:对集合的子集执行操作

OData 的 Addressing a Subset of a Collection 特性允许在 URL 的一个路径段中放置$filter,而不是把它作为查询字符串的一部分。这个特性在"集合上存在操作,且客户端希望对集合的某个子集执行该操作"时非常有用。

以 Microsoft Graph 的riskyUsersAPI 为例:该 API 上定义了一个名为dismiss的动作(Action),用于把一批风险用户标记为"不再有风险"。在 CSDL 元数据中,这个动作绑定在Collection(microsoft.graph.riskyUser)类型的绑定参数上,并通过userIds参数接收要处理的用户 ID 列表:

<Action Name="dismiss" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(microsoft.graph.riskyUser)" /> <Parameter Name="userIds" Type="Collection(Edm.String)" /> </Action>

动作是绑定操作(bound operation),其第一个参数永远是绑定参数;按照仓库 operations 模式文档 的说明,Microsoft Graph 只支持绑定动作与绑定函数,因此IsBound="true"和绑定参数是必须的。客户端调用该动作时使用 POST:

POST /identityProtection/riskyUsers/dismiss { "userIds": [ "{userId1}", "{userId2}", ... ] }

这种建模方式的问题在于:dismiss动作只能按"显式列举的 userId 集合"工作。如果客户端想按其他条件(例如风险等级、检测来源、最近一次风险时间)来筛选要撤销的用户,服务团队就不得不为每一种新条件实现一个新的dismiss重载——这既不可扩展,也增加了 API 面。

方案一:直接使用 filter-as-segment + 参数别名

利用 OData 的 filter-as-segment 特性,上述动作可以改造成不再接收userIds参数,过滤条件完全交给 URL 路径段中的$filter表达式:

<Action Name="dismiss" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(self.riskyUser)" /> </Action>

这里需要注意类型前缀的变化:self前缀指向当前命名空间中的riskyUser类型(与microsoft.graph.riskyUser等价,self是 CSDL 中表示当前命名空间的缩写)。客户端调用时,把$filter放在路径段中,并借助参数别名(parameter alias)@f承载过滤表达式:

POST /identityProtection/riskyUsers/$filter=@f/dismiss?@f=id IN ('{userId1}','{userId2}',...)

这种做法的显著优势来自 OData 过滤表达式本身的健壮性:客户端可以基于任何受支持的过滤条件来撤销风险用户,而服务团队不需要针对每一种新条件去新增dismiss的重载实现。$filter表达式的语义在仓库的 collections 文档 中有完整定义——表达式针对集合中的每个资源求值,只有求值为 true 的资源才会被纳入结果集,求值为 false、null 或引用了无权限属性的资源都会被剔除;支持的运算符包括eq、ne、gt、ge、lt、le、and、or、not与括号分组,并遵循 OData 规定的优先级顺序。

方案二(推荐):引入可组合的 filter 函数

尽管 filter-as-segment 能力强大,但指南明确指出它存在两方面顾虑:

  • 可发现性(discoverability):过滤条件被藏进 URL 段和别名参数中,相比显式的函数参数,客户端与服务端对"这个操作到底接受什么输入"的感知更弱,元数据(CSDL)也无法直接体现可用的过滤能力。
  • 参数别名的支持程度:filter-as-segment 通常需要配合参数别名(如上面的@f)把长表达式从路径段挪到查询字符串中,而不同服务端对参数别名的支持并不一致。

因此,指南的结论是:应当引入一个以与 filter-as-segment 相同方式工作的函数(Function),把过滤表达式作为显式的字符串参数暴露出来:

<Function Name="filter" IsBound="true" IsComposable="true"> <Parameter Name="bindingParameter" Type="Collection(microsoft.graph.riskyUser)" Nullable="false" /> <Parameter Name="expression" Type="Edm.String" Nullable="false" /> <ReturnType Type="Collection(microsoft.graph.riskyUser)" /> </Function>

这个定义中的几个关键点,与仓库 operations 模式文档 中关于函数/动作的规则一一对应:

  • IsBound="true":函数绑定在Collection(microsoft.graph.riskyUser)类型的绑定参数上,只能在riskyUsers集合(或其子路径)上调用;
  • IsComposable="true":函数是可组合的,即它的返回值(一个riskyUser集合)可以继续作为后续路径段的资源,这正是我们能在filter(...)之后继续追加/dismiss动作的技术前提;
  • Nullable="false":绑定参数与expression参数均不可为空。这里尤其要注意:依据仓库 GuidelinesGraph.md 的 breaking changes 清单,"向已有动作添加未标记为 Nullable 的参数"属于破坏性变更,所以从一开始就用非空参数建模是稳妥的做法;
  • expression为Edm.String:整个 OData 过滤表达式以字符串形式传入,客户端可以使用任意受支持的过滤语法。

客户端调用时,把过滤表达式放进函数括号内:

POST /identityProtection/riskyUsers/filter(expression='id IN (''{userId1}'',''{userId2}'',...)')/dismiss

注意(NOTE):由于过滤表达式是包在单引号字符串里的,表达式内部的字面量单引号'必须用''(两个单引号)转义——这正是上面示例中''{userId1}''的由来。OData 字符串字面量的单引号转义规则同样适用于此。

filter函数本身是纯函数(无副作用、返回集合),因此在仅用于检索时它同样可以通过 GET 调用(例如GET /identityProtection/riskyUsers/filter(expression='riskLevel eq ''high''')直接获取过滤后的集合);本文示例中整个请求是 POST,是因为 URL 链路的最终目的是在过滤结果上继续执行dismiss动作——动作按照 operations 模式文档 的规则必须用 POST 调用。

为什么函数形态优于裸的 filter-as-segment

除了前文提到的可发现性与参数别名支持问题,从 API 契约与演进的角度看,函数形态还有额外的好处:

  1. 契约自描述:expression参数出现在 CSDL 元数据中,SDK 生成器与客户端工具能够识别这个操作的存在与输入类型,而 filter-as-segment 的$filter=@f/...段对元数据来说是不可见的。
  2. 避免无休止的重载:如果走"为每种过滤条件新增 dismiss 重载"的老路,每一次新增过滤维度都是一次 API 面扩张,并且——根据仓库 GuidelinesGraph.md 的定义——给已有操作新增必选参数属于破坏性变更,需要版本化与弃用流程。而filter函数把过滤能力收敛为一个字符串参数,任何新的过滤维度都只是"表达式写法"的变化,无需触碰契约。
  3. 与操作模式一致:Microsoft Graph 的 operations 模式 明确指出,无副作用且返回单个/集合实例的操作应建模为 OData 函数;filter恰好就是这样一个操作。
  4. 命名合规:函数名filter遵循仓库 naming 指南 的 lowerCamelCase 规则,作为集合上的通用操作名简洁且表意清晰。

与集合子集建模模式的呼应

filter函数解决的是"在请求时用表达式划定集合子集"的问题;而在 API 建模层面,仓库还提供了另一个互补的模式:Modeling collection subsets。该模式用抽象基类 + 派生类型(如allMembership、enumeratedMembership、noMembership、excludedMembership)来表达"全部/部分/排除/无"等集合子集状态,适合把子集定义持久化为资源模型的一部分(例如条件访问策略中的成员范围)。当子集需要由客户端在调用时临时指定时,filter函数是最直接的手段;当子集需要作为状态长期保存并支持查询时,subsets 模式更合适——两者一个偏向"操作时过滤",一个偏向"建模时固化",可以按场景组合使用。

实现参考

OData WebApi(AspNetCoreOData代码库)中提供了该filter函数的一个示例实现(对应提交7732f7e6b812d9a79a73529562f2e74b68e2794f),可作为服务端实现本模式的起点:它演示了如何在 CSDL 中声明绑定在集合上的可组合filter函数、如何解析expression字符串参数,并将其作为后续路径段(如/dismiss)继续路由。在搭建自己的 OData 服务时,可以对照该实现验证IsComposable行为与表达式求值的正确性。

实践要点小结

  • 需要"对集合子集执行动作"时,优先考虑把动作绑定参数设为集合类型,过滤交给路径段而非动作参数;
  • 相比直接使用$filter=段 + 参数别名,推荐建模一个IsBound="true"、IsComposable="true"、以Edm.String接收表达式并返回同型集合的filter函数;
  • 绑定参数与expression参数均声明为Nullable="false",避免后续演进时落入"新增非空参数 = 破坏性变更"的陷阱;
  • 表达式字符串内的单引号字面量必须用''转义;
  • filter函数用于检索时用 GET,作为动作链路的前置段时整个请求用 POST;
  • 过滤表达式的语法与运算符语义遵循仓库 collections 文档 中的约定。
  • API设计

【免费下载链接】api-guidelines

Microsoft REST API Guidelines

项目地址:https://gitcode.com/gh_mirrors/ap/api-guidelines
点击查看免费下载
上一篇:Carbon Design System v10 到 v11 迁移指南:Sass Modules 重构、组件 API 变更与 Codemod 自动化迁移
下一篇:Transmission种子健康度终极指南:让下载速度提升300%的完整教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询