Microsoft Graph 的 Facets 设计模式:用复合类型刻画多态资源变体

原创2026-09-30 23:16:291,744 阅读
文章标签:API设计

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)就无法表达"既是文件又是视频"这一事实。

解决方案:实体类型 + 每个变体一个复合类型

原文档给出的解决思路分两步:

  1. 为每个变体创建独立的复合类型(complex type),把该变体的属性打包在一起;
  2. 在实体类型上为每个复合类型声明一个同名属性,用来承载该变体的属性。

由于复合类型属性作为实体结构的一部分存在,客户端可以直接访问 实体.变体属性.具体字段,而子变体的识别完全依赖"父对象上是否存在一个或多个 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 的补充,演进时需要特别注意:

  1. 语义变化即破坏性变更:新增 facet 可能让客户端此前"仅一个 facet 非空"的假设被打破(旧 facet 全部为 null、新 facet 出现),从而改变客户端对对象语义的理解。API 设计应提前规划演化路径(详见 GuidelinesGraph.md 关于"改变语义永远是破坏性变更"的说明)。
  2. 可空属性问题:facets 与 flat bag 模式通常要求 facet 属性可空(因为"不存在即不代表该变体"),因此要克制地只在必要处使用可空属性,具体约束参见 nullable.md。
  3. 元数据刻画是"部分的":从模式对比表可知,facets 在元数据中只部分描述属性与行为(不像类型层次那样完整捕获"哪些属性属于哪些变体"的依赖关系),模型语义更多依赖文档与命名约定来传达。

相关资源

登录后查看全文
api-guidelines