Microsoft Graph 资源建模模式详解:Flat Bag of Properties 扁平属性包模式

原创2026-09-30 21:31:021,700 阅读
文章标签:API设计

Microsoft Graph 资源建模模式详解:Flat Bag of Properties 扁平属性包模式

本指南聚焦 Microsoft Graph API 设计模式库中的 Flat bag of properties(扁平属性包) 模式,讲解如何用"单个实体类型 + 全部潜在属性 + 一个区分变体的属性"来建模同一概念的多个变体。你将掌握该模式的定义、适用场景、取舍权衡,以及以 recurrencePattern 为代表的真实实现细节,并学会在与 Type hierarchy(类型层级)、Facets(切面)两种模式之间做出正确选择。

模式概述:把多个变体装进一个"扁平口袋"

Microsoft Graph API 设计模式库把资源建模中"同一概念存在多个变体"的场景归纳为三种主流模式:类型层级、切面、扁平属性包。其中扁平属性包的定义可以概括为一句话:

A known pattern in Microsoft Graph is to model multiple variants of a common concept as a single entity type with all potential properties plus an additional property to distinguish the variants.

即:用一个实体类型承载该概念所有潜在属性,并额外增加一个属性(常命名为 variantType 或 type)来区分各个变体。对于 type 的每一个取值,只有部分属性是有意义的,其余属性在该变体下被忽略。

从 graph/GuidelinesGraph.md 的模式总览来看,该模式被正式定位为三种变体建模手段之一:

Flat bag of properties is represented by one entity type with all the potential properties plus an additional property to distinguish the variants, often called type. The type property describes the variant and also defines properties that are required or meaningful for the variant given by the type property.

由于所有属性直接"平铺"在同一个类型上、不借助继承或嵌套复杂类型,它被形象地称为"扁平口袋"(flat bag)。

要解决的问题(Problem)

根据 graph/patterns/flat-bag.md 的描述,该模式针对的设计场景是:

  • API 设计者需要建模数量少且有限的、属于同一概念的变体;
  • 各变体的属性互不重叠(non-overlapping),行为保持一致;
  • 设计者同时希望简化查询构造(simplify query construction)。

换言之,当变体数量可控、属性清单简洁、且客户端主要以查询(尤其是只读查询)方式使用资源时,设计者需要一个比继承体系更"轻"的建模方案。

解决方案(Solution)

方案的实现要点(见 graph/patterns/flat-bag.md):

  1. 创建一个实体类型,把该概念所有变体的潜在属性全部作为该类型的属性列出;
  2. 增加一个额外的区分属性(通常命名为 variantType,在 Microsoft Graph 实际实现中常命名为 type);
  3. 约定语义:对于 type 的每个取值,一部分属性是 meaningful(有意义)的,其余属性被 ignored(忽略)。

从 CSDL(Common Schema Definition Language)层面看,属性之间的"哪些属性属于哪个变体"这种依赖关系完全没有被类型系统捕获——这正是该模式与类型层级(依赖关系完整记录在元数据中)最本质的区别。

何时使用该模式(When to use this pattern)

按 graph/patterns/flat-bag.md 的指引,满足以下条件时可以考虑扁平属性包:

  • 变体数量少,且各变体的行为相似;
  • 变体主要用于只读查询类操作;
  • 希望利用 OData $filter 表达式语法层面更简单地查询资源——因为所有属性都在同一类型上,**无需类型转换(casting)**即可直接过滤。

最后一点是关键优势:相比之下,类型层级模式若要按子类型属性过滤,必须在 $filter 中使用 microsoft.graph.user/jobTitle 这样的全限定名转换段(详见 graph/patterns/subtypes.md),而扁平属性包中的属性直接可用 $filter=daysOfWeek eq ... 形式过滤。

问题与考虑(Issues and considerations)

该模式是 Microsoft Graph 模式库中明确标注为最不推荐的建模选择之一,理由集中在弱类型与语义验证困难上(见 graph/patterns/flat-bag.md):

  • 弱类型(weakly typed):属性与变体的对应关系只存在于文档和约定中,不由类型系统表达,客户端无法从元数据得知某个属性对当前变体是否有效;
  • 难以语义化验证定向资源修改:由于服务端难以静态判断"这个属性在这个变体下是否有意义",针对单个属性的 PATCH 修改很难做语义层面的校验;
  • 不适用于大量变体与大量属性:当变体数量增多、属性清单膨胀时,绝大多数请求的 payload 会变得稀疏填充(sparsely populated)——大量属性为 null 或缺失,既浪费带宽也影响可读性。

不过在"查询简单 + 属性数量有限"压倒强类型考虑的场景下,该模式仍然是被接受的。官方文档也建议评估相关模式作为替代:类型层级 subtypes.md 与切面 facets.md。值得注意的是,模式库的 antiPatternTemplate.md 甚至将其作为"已知反模式"(known anti-pattern)的示例引用,用于演示如何编写反模式描述模板——这再次印证了它"能用但需克制"的定位。

实战示例:recurrencePattern 扁平属性包

graph/patterns/flat-bag.md 给出的官方示例是 recurrencePattern 类型——Microsoft Graph 中用于描述定期事件(如会议、任务、访问评审)重复规则的模型。它是扁平属性包模式的教科书式实现:一个复杂类型 + 六个变体 + 一个 type 属性。

变体枚举:recurrencePatternType

六个变体通过 recurrencePatternType 枚举的六个成员表达(数值 0–5 为 OData 枚举值):

<EnumType Name="recurrencePatternType">
        <Member Name="daily" Value="0" />
        <Member Name="weekly" Value="1" />
        <Member Name="absoluteMonthly" Value="2" />
        <Member Name="relativeMonthly" Value="3" />
        <Member Name="absoluteYearly" Value="4" />
        <Member Name="relativeYearly" Value="5" />
</EnumType>

六个取值及含义如下表:

枚举成员 数值 语义
daily 0 按日重复
weekly 1 按周重复
absoluteMonthly 2 按月重复(固定日期,如每月 5 日)
relativeMonthly 3 按月重复(相对日期,如每月第一个周一)
absoluteYearly 4 按年重复(固定日期,如每年 3 月 5 日)
relativeYearly 5 按年重复(相对日期,如每年 12 月的最后一个工作日)

实体(复杂类型)定义:全部属性平铺

recurrencePattern 在 CSDL 中被定义为 ComplexType,所有变体的潜在属性全部平铺其中:

<ComplexType Name="recurrencePattern" ags:WorkloadIds="Microsoft.Exchange,Microsoft.IdentityGovernance.AccessReviews,Microsoft.IGAELM,Microsoft.PIM.AzureRBAC,Microsoft.Tasks,Microsoft.Teams.Shifts,Microsoft.Todo" ags:PassthroughWorkloadIds="Microsoft.Exchange">
        <Property Name="dayOfMonth" Type="Edm.Int32" Nullable="false" />
        <Property Name="daysOfWeek" Type="Collection(graph.dayOfWeek)" />
        <Property Name="firstDayOfWeek" Type="graph.dayOfWeek" />
        <Property Name="index" Type="graph.weekIndex" />
        <Property Name="interval" Type="Edm.Int32" Nullable="false" />
        <Property Name="month" Type="Edm.Int32" Nullable="false" />
        <Property Name="type" Type="graph.recurrencePatternType" />
</ComplexType>

逐属性解读:

属性 类型 可空性 在该模型中的角色
dayOfMonth Edm.Int32 不可空 每月/每年的固定日期(对应 absolute 变体)
daysOfWeek Collection(graph.dayOfWeek) 可空 一周中的哪些天(对应 weekly 变体)
firstDayOfWeek graph.dayOfWeek 可空 一周起始日(配合 daysOfWeek/weekly 使用)
index graph.weekIndex 可空 相对变体的"第几个"(如 first/second/last,配合 relative 变体)
interval Edm.Int32 不可空 重复间隔(如每 3 天、每 2 周)
month Edm.Int32 不可空 月份(对应 yearly 变体)
type graph.recurrencePatternType 可空 区分六个变体的判别属性

属性上的 ags:WorkloadIds 与 ags:PassthroughWorkloadIds 是 Microsoft Graph 内部的 workload(工作负载)标注,说明该类型被 Exchange、IdentityGovernance.AccessReviews、Teams.Shifts、Todo 等多个工作负载共享或透传——这也从侧面解释了为何用扁平结构统一建模比各负载各自定义子类型更省事。

属性与变体的对应关系:部分有意义、部分被忽略

官方文档明确指出(graph/patterns/flat-bag.md):对于每个 type 取值,只有部分属性有意义,其余被忽略。例如 daysOfWeek 在 type = weekly 时有意义,在 type = daily 时则被忽略。

从 CSDL 结构可以进一步推断各变体的属性使用情况:

  • type = daily:核心属性为 interval(每 N 天),daysOfWeek、index、month 等均无意义;
  • type = weekly:核心属性为 interval(每 N 周)、daysOfWeek(哪些天)、firstDayOfWeek(周起始日);
  • type = absoluteMonthly:核心属性为 interval(每 N 个月)、dayOfMonth(固定日);
  • type = relativeMonthly:核心属性为 interval、index(weekIndex,如"第一个")、daysOfWeek(哪一天);
  • type = absoluteYearly:核心属性为 month、dayOfMonth;
  • type = relativeYearly:核心属性为 month、index、daysOfWeek。

也就是说,一次实际请求的 payload 中,只有与当前 type 对应的属性会被填充,其余属性被服务端忽略或返回为空。一个 weekly 变体的示意性 JSON 序列化如下(示意,非官方响应样例):

{
  "type": "weekly",
  "interval": 2,
  "daysOfWeek": ["monday", "wednesday", "friday"],
  "firstDayOfWeek": "monday"
}

可以看到,dayOfMonth、month、index 等属性在该变体下不参与语义。

查询优势的体现

由于所有属性平铺在同一类型上,客户端可以直接对 daysOfWeek、interval 等属性书写 $filter,无需任何转换段。这与 graph/patterns/subtypes.md 中展示的类型层级查询形成鲜明对比——后者必须写成 $filter=microsoft.graph.user/jobTitle eq 'CEO' 或 /members/microsoft.graph.user?$filter=... 才能访问子类型属性。扁平属性包的查询简洁性正是其核心卖点。

与相关模式的对比:何时放弃扁平属性包

graph/GuidelinesGraph.md 从"元数据表达能力、变体组合支持、查询构造难度"三个维度对三种模式做了正式对比,是决策的核心依据:

API 质量 \ 模式 属性和行为是否记录在元数据中 是否支持属性/行为的组合 查询构造是否简单
Type hierarchy(类型层级) 是 否 否
Facets(切面) 部分 是 是
Flat bag(扁平属性包) 否 否 是

配套的优缺点分析(见 graph/GuidelinesGraph.md)对扁平属性包的定位给出了更完整的视角:

  • 类型层级把"哪些属性属于哪些变体"的依赖关系完整记录在元数据中,强类型语言客户端可据此构造/校验请求;新增变体相对隔离,语法上向后兼容。
  • 切面可以建模"一个对象同时是多种变体"(如 OneDrive 中的 driveItem 既是 file 又是 video),具备类似多重继承的表达能力,详见 graph/patterns/facets.md。
  • 扁平属性包与切面都带来语法上更简单的 $filter,但类型层级更显式、需要转换段。
  • 从客户端语言角度看,类型层级和切面更适合强类型编程语言,而扁平属性包更贴近弱类型语言开发者的习惯(见 graph/GuidelinesGraph.md)。

因此在选择时可以参考以下决策线索:

  • 变体互斥、数量少、以查询为主 → 可考虑扁平属性包;
  • 变体互斥、需要完整语义元数据、强类型 SDK 优先 → 选类型层级;
  • 变体不互斥(可同时成立,如"既是文件又是视频")→ 选切面;
  • 变体数量多、属性清单大 → 三者都需谨慎,但扁平属性包最先被排除(payload 稀疏问题)。

与可空属性的关系

graph/GuidelinesGraph.md 特别提醒:切面和扁平属性包两种方案往往需要大量可空属性,因此设计时仍应在适当位置坚持使用不可空属性;同时,继承(类型层级)常常能完全消除对可空属性的依赖。

理解 graph/articles/nullable.md 中的定义有助于把握其中的分寸:可空(nullable)仅表示属性值可以是 null,并不说明该属性在创建时是否必需;不可空属性也不代表创建时必填,只是"检索时一定会有值"。回到 recurrencePattern 示例:interval、dayOfMonth、month 被标记为 Nullable="false",因为无论哪个变体,这些基础周期参数在语义上都应有值;而 daysOfWeek、index、firstDayOfWeek 等变体专属属性则可空——这正是"变体相关属性用可空、公共/核心属性保持不可空"的落地写法。

演进与兼容性:新增变体的代价

从演进角度看,三种模式各有取舍(见 graph/GuidelinesGraph.md):

  • 在切面中新增一个 facet、在类型层级中新增一个子类型、在扁平属性包中新增一个 type 取值,三者本质上都会引入"旧对象语义可能被改变"的风险(例如原本某个属性非空,现在新变体下它应为空);
  • 扁平属性包新增 type 值本身在语法上是向后兼容的(枚举新增成员,配合 evolvable-enums.md 的演进枚举机制),但语义层面仍需谨慎:如果新增取值导致既有属性的语义在新取值下发生变化,就属于破坏性变更——正如指南反复强调的:改变语义永远是一次破坏性变更。

此外,弱类型特性还带来一个隐性成本:强类型语言 SDK 无法从元数据自动获知"哪个属性对哪个变体有效",客户端需要依赖文档和约定自行处理,这在 GA(正式发布)API 上会增加长期维护与沟通成本。因此模式库的总体态度是:查询简洁性和有限属性数量只有在明显压倒强类型考虑时,才值得采用扁平属性包。

总结:扁平属性包的决策清单

决策问题 答案 倾向
变体数量是否少(个位数)且有限? 是 可用
变体是否互斥? 是 可用(互斥时可考虑层级)
是否以只读查询为主? 是 加分($filter 无需 cast)
是否需要强类型元数据/严格语义校验? 否 加分
属性清单是否膨胀、payload 是否会变稀疏? 否 可用;是则放弃
是否需要支持"多变体并存"(组合)? 是 放弃,改用 facets.md
是否需要完整语义元数据支撑强类型 SDK? 是 放弃,改用 subtypes.md

recurrencePattern 之所以最终选择了扁平属性包,正是因为它的六个变体数量少、互斥、以查询语义为主,且各变体的专属属性数量有限——是"查询简单性压倒强类型考虑"这一权衡原则的最佳注脚。当你面对类似的建模场景时,可以以它为参照,对照上述清单做出决策。

登录后查看全文
api-guidelines