Microsoft Graph 的 Facets 设计模式:用复合类型刻画多态资源变体
Microsoft Graph 的 Facets 设计模式:用复合类型刻画多态资源变体
本文围绕 facets.md 展开,系统讲解 Microsoft Graph 中用于建模"同一概念的多个非互斥变体"的 Facets 设计模式:如何用一个实体类型承载多个复合类型(facet)属性、如何通过 facet 是否存在来识别资源变体,以及它在查询(
$filter免类型转换)、演进和强类型客户端中的实际收益与代价。读完本文,你将掌握在 OData/CSDL 层面设计多态资源模型的方法,并能在 Type hierarchy(类型层次)与 Flat bag(扁平属性包)之间做出有依据的模式选型。
模式概览与核心思想
Facets 是 Microsoft Graph 中最常被使用的三种资源建模模式之一(另外两种是 Type hierarchy 与 Flat bag of properties,见 GuidelinesGraph.md 的归纳):
把同一概念的多个变体建模为一个实体类型:实体拥有公共属性,并为每一个变体声明一个facet 属性(类型为复合类型 complex type),用于承载该变体专属的属性。
其核心机制是:
- 实体类型的公共属性描述所有变体共有的信息;
- 每个变体的专有属性被装进独立的复合类型(facet)中,作为实体上的一个属性存在;
- 某个变体是否成立,通过父对象上是否存在对应的 facet 来判断——facet 属性只在对象确实代表该变体时才被填充(有值),否则为 null。
一个变体可以同时拥有多个 facet,因为变体之间不互斥。这正是 facets 与类型层次(subtypes 互斥)最本质的区别。
问题:异构资源与非互斥变体
当一个 API 设计者需要建模一组异构资源时,典型困境是:这些资源共享公共属性与公共行为,但各自又有专属属性;更麻烦的是,变体之间不互斥——同一个对象可能同时表达多个变体的特征。
原文档给出的例子非常直观:存放在 OneDrive 中的一个电影剪辑(movie clip),既是一个文件,又是一个视频。文件有 mimeType、hashes 等文件专属属性,视频有时长、宽高、帧率等视频专属属性,而它们都属于同一个"驱动器中的条目"概念。此时,把文件与视频建模为互斥的子类型(subtype)就无法表达"既是文件又是视频"这一事实。
解决方案:实体类型 + 每个变体一个复合类型
原文档给出的解决思路分两步:
- 为每个变体创建独立的复合类型(complex type),把该变体的属性打包在一起;
- 在实体类型上为每个复合类型声明一个同名属性,用来承载该变体的属性。
由于复合类型属性作为实体结构的一部分存在,客户端可以直接访问 实体.变体属性.具体字段,而子变体的识别完全依赖"父对象上是否存在一个或多个 facet"。
这里有必要补充 Microsoft Graph 对实体类型与复合类型的边界约定(见 GuidelinesGraph.md):
- 实体类型(EntityType) 声明 key 属性(Microsoft Graph 中通常为
id),对象可被 URI 直接寻址; - 复合类型(ComplexType) 不声明 key,没有唯一标识符,不可被 URI 直接寻址,适合用来表示实体的"复合属性"——facets 模式正是把复合类型用作实体属性容器的典型场景。
因此 facets 模式天然满足"变体属性随实体一起返回、无需额外寻址"的诉求。
何时使用该模式
根据 facets.md 的界定,facets 模式在以下场景最适用:
- 变体数量较多,且变体之间不互斥(一个对象可同时命中多个变体);
- 希望通过 OData
$filter表达式便捷地查询资源——因为 facet 就是实体上的普通属性,查询不需要类型转换(casting)。对比之下,类型层次模式在$filter中引用子类型属性时必须使用转换段(如microsoft.graph.user/jobTitle,参见 subtypes.md)。
同时应结合候选模式一起权衡:Type hierarchy(类型层次) 适合"变体互斥、且需要强类型元数据约束"的场景;Flat bag(扁平属性包) 适合"变体数量少、属性列表简短、以只读查询为主"的场景。
三种变体建模模式的系统性对比
GuidelinesGraph.md 给出了一张模式选型对照表,直接回答了"三个模式各自在元数据刻画、组合支持、查询简洁度上的表现":
| API 特质 \ 模式 | 属性与行为在元数据中的刻画 | 支持属性/行为的组合 | 查询构造简单 |
|---|---|---|---|
| Type hierarchy(类型层次) | 完整(yes) | 否(no) | 否(no) |
| Facets | 部分(partially) | 是(yes) | 是(yes) |
| Flat bag(扁平属性包) | 否(no) | 否(no) | 是(yes) |
结合 GuidelinesGraph.md 的 Pros/Cons 分析,facets 模式还有如下特性值得注意:
- 新增变体很直接:只需新增一个复合类型属性即可扩展模型;但需小心,这可能在客户端造成"此前仅一个 facet 非空、此后旧 facet 全部为 null"的语义变化——这与类型层次新增子类型、扁平包新增
type枚举值所带来的风险类似; - 对强类型客户端语言友好(略逊于类型层次,但明显优于扁平包),因为每个 facet 是具名复合类型,SDK 可以生成对应的强类型对象;
- 具备模拟多重继承的潜力:一个实体可同时填充多个 facet,近似于"同时继承多个父类特征";
$filter语法简洁:直接引用 facet 属性即可,无需类型转换段。
实战示例:driveItem 实体的多 facet 建模
原文档的完整示例是 OneDrive 的 driveItem 资源——它代表驱动器中的文件、文件夹、图片或其他条目,是 facets 模式在 Microsoft Graph 中的典型落地。下面是原文档完整的 CSDL 实体定义(节选),可以看到实体为每一个变体声明了一个复合类型属性:
<EntityType Name="driveItem" BaseType="graph.baseItem" OpenType="true" ags:MasterService="Microsoft.FileServices" ags:WorkloadIds="Microsoft.Excel,Microsoft.Powerpoint,Microsoft.Teams.GraphSvc,Microsoft.Word">
<Property Name="audio" Type="graph.audio" />
<Property Name="bundle" Type="graph.bundle" />
<Property Name="content" Type="Edm.Stream" />
<Property Name="cTag" Type="Edm.String" />
<Property Name="deleted" Type="graph.deleted" />
<Property Name="file" Type="graph.file" />
<Property Name="fileSystemInfo" Type="graph.fileSystemInfo" />
<Property Name="folder" Type="graph.folder" />
<Property Name="image" Type="graph.image" />
<Property Name="location" Type="graph.geoCoordinates" />
<Property Name="malware" Type="graph.malware" />
<Property Name="media" Type="graph.media" />
<Property Name="package" Type="graph.package" />
<Property Name="pendingOperations" Type="graph.pendingOperations" />
<Property Name="photo" Type="graph.photo" />
<Property Name="publication" Type="graph.publicationFacet" />
<Property Name="remoteItem" Type="graph.remoteItem" />
<Property Name="root" Type="graph.root" />
<Property Name="searchResult" Type="graph.searchResult" />
<Property Name="shared" Type="graph.shared" />
<Property Name="sharepointIds" Type="graph.sharepointIds" />
<Property Name="size" Type="Edm.Int64" />
<Property Name="source" Type="graph.driveItemSource" />
<Property Name="specialFolder" Type="graph.specialFolder" />
<Property Name="video" Type="graph.video" />
<Property Name="webDavUrl" Type="Edm.String" />
...
</EntityType>
几个值得注意的细节:
- 复合类型属性(
file、folder、image、video、audio、photo、malware、shared等)与普通标量属性(size、cTag、webDavUrl)并列存在,facet 仅仅是其中"装变体属性"的那部分; BaseType="graph.baseItem"表明公共属性(如id、name、createdDateTime)来自基类型,facet 负责差异部分——这正是"公共属性 + 变体 facet"的完整组合;OpenType="true"表示该类型是开放类型,允许实例携带未在元数据中声明的属性,进一步增强了模型的扩展性;- 元数据注解
ags:MasterService、ags:WorkloadIds标明了该实体由哪个后端服务主责、哪些工作负载使用,属于 Microsoft Graph 元数据治理的一部分。
异构集合的返回效果
请求个人 OneDrive 根目录下所有条目,返回的是一个异构集合——不同条目填充了不同的 facet。原文档的示例响应(为可读性做了精简)中恰好包含三类变体:一个文件夹(folder facet)、一个 Word 文档(file facet)、一张 JPEG 图片(同时填充 file 与 image 两个 facet):
GET https://graph.microsoft.com/v1.0/me/drive/root/children
Response shortened for readability:
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('93816c1c-1b19-41de-a322-a1643d7f4d39')/drive/root/children",
"value": [
{
"createdDateTime": "2021-07-07T13:59:47Z",
"name": "Microsoft Teams Chat Files",
...,
"folder": {
"childCount": 15
}
},
...
{
"createdDateTime": "2021-12-15T00:07:36Z",
"name": "Versioning and Deprecation.docx",
...,
"file": {
"mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"hashes": {
"quickXorHash": "r2d9uZilW0zEIXwycymsUQzhV+U="
}
},
...
},
{
"createdDateTime": "2021-12-21T16:32:51Z",
"name": "WhaleShark.jpg",
...
"file": {
"mimeType": "image/jpeg",
"hashes": {
"quickXorHash": "2vHpAA7RDZJteIwl1pXR980xuh4="
}
},
...,
"image": {}
}
]
注意"WhaleShark.jpg"这条记录:file 与 image 两个 facet 同时有值——这正是 facets 模式支持"变体不互斥"的直接证据,也印证了原文档开篇"一个电影剪辑既是文件又是视频"的问题场景。客户端无需任何类型判断,仅凭 JSON 中 image 字段存在与否即可识别这是一张图片;同理,folder.childCount 的存在标识文件夹。
查询优势:免类型转换的 $filter
与类型层次模式相比,facets 模式在查询上的核心优势是不需要转换段(casting)。类型层次模式中,要按子类型属性过滤,必须在属性名前加完全限定类型名(如 $filter=microsoft.graph.user/jobTitle eq 'CEO',参见 subtypes.md);而 facets 模式下,facet 是实体上的普通复合类型属性,可直接参与过滤。例如针对上述 OneDrive 场景,可以写出如下形式的过滤表达式(示意原文档"不需要 casting"这一论断的语法形态):
GET https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=file/mimeType eq 'image/jpeg'
由于变体通过"facet 是否存在/是否非空"表达,也可以直接以存在性为条件过滤(例如筛选出所有带 image facet 的条目)。这种语法简洁性正是 GuidelinesGraph.md 所指出的 "facets 与 flat bag 能带来语法上更简单的 filter 表达式,而 hierarchy 更显式但需要 cast 段"。
注意事项与演进风险
原文档明确提示:引入新的 facet 时,必须确保新 facet 不会通过隐式约束改变模型的语义。结合 GuidelinesGraph.md 的补充,演进时需要特别注意:
- 语义变化即破坏性变更:新增 facet 可能让客户端此前"仅一个 facet 非空"的假设被打破(旧 facet 全部为 null、新 facet 出现),从而改变客户端对对象语义的理解。API 设计应提前规划演化路径(详见 GuidelinesGraph.md 关于"改变语义永远是破坏性变更"的说明)。
- 可空属性问题:facets 与 flat bag 模式通常要求 facet 属性可空(因为"不存在即不代表该变体"),因此要克制地只在必要处使用可空属性,具体约束参见 nullable.md。
- 元数据刻画是"部分的":从模式对比表可知,facets 在元数据中只部分描述属性与行为(不像类型层次那样完整捕获"哪些属性属于哪些变体"的依赖关系),模型语义更多依赖文档与命名约定来传达。
相关资源
- 模式原文:facets.md
- 姊妹模式:Type hierarchy、Flat bag of properties
- 模式总览与选型表:GuidelinesGraph.md
- 实体/复合类型建模规则:GuidelinesGraph.md
- 可空属性指引:nullable.md
- 核心类型扩展限制(facet 属性不应滥加在核心类型上):coreTypes.md
- 模式写作模板:PatternDescriptionTemplate.md