news 2026/10/1 9:17:03

Microsoft Graph 核心类型(Core Types)建模指南:为何 user、group、device 不应被随意扩展属性

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Microsoft Graph 核心类型(Core Types)建模指南:为何 user、group、device 不应被随意扩展属性
  • API设计

【免费下载链接】api-guidelines

Microsoft REST API Guidelines

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

导读

本指南聚焦 Microsoft Graph REST API 设计规范中的一个关键约束:user、group、device三种核心类型是整个 Graph 生态中连接度最高的中心实体,因此 API 设计者不得出于便利而向它们随意追加结构属性。文章将完整讲解这一限制的动机、三种官方推荐的替代建模方案(导航属性建模的三种形态),并结合仓库中的 Navigation Property 模式、GuidelinesGraph.md 总纲 及配套的类型层次、Facets、Flat Bag 等模式文档,给出可直接落地到 CSDL 模型的 XML 示例与取舍判断标准。读完本文,你将掌握如何在 Microsoft Graph 生态内正确扩展核心实体、避免破坏 API 契约的设计陷阱。

一、什么是 Microsoft Graph 的"核心类型"

Microsoft Graph 中存在一类高度连接、处于整个生态中心位置的类型。它们在 Graph 中与大量实体存在关联,因此天然处于"能够容纳与其他 API 相关的结构属性"的位置——其他工作负载(workload)在建模时很容易产生"顺手把这个字段挂到 user 上"的想法。

在 coreTypes.md 中,被正式认定为核心类型的只有三个:

  • user
  • group
  • device

这三类实体在 Microsoft Graph 中被识别为核心类型,在所有场景下,向它们新增结构属性都需要提供强有力的论证("will require strong justification")。这是 GuidelinesGraph.md 的 Limitations on core types 章节 的细化规则,总纲原文即明确:user、group、device不应在没有充分理由的情况下新增任何结构属性(structural properties)。

核心原则:内在属性 vs 便利属性

判断能否向核心类型追加属性的唯一标准是属性是否实体本身固有(intrinsic):

Structural properties should be only added to these core types when they are intrinsic to the entity itself, and strictly not for the purpose of convenience due to the entity's position in Microsoft Graph.

即:只有当属性是实体自身天然携带、不可剥离的特征时才允许添加;严格禁止因为该实体处于 Graph 的中心位置、恰好方便挂载,就把它当作"杂物收纳箱"使用。

例如,user上的displayName、jobTitle、mail属于用户实体内在特征,可以存在;而"某用户的银行账户信息""某用户的会员等级"这类依附于用户、却并非用户固有属性的数据,就应当走替代建模方案。

二、替代方案总览:用"新类型 + 导航属性"建模

既然不能往user、group、device上加结构属性,正确的做法是:

  1. 创建一个新类型,把拟新增结构属性所承载的信息建模进去;
  2. 在核心类型与新类型之间用导航属性(navigation property)建模关系。

导航属性是 OData/CSDL 中用于描述"资源与资源之间关系"的机制,其定义与使用细节详见 Navigation Property 模式。它的核心价值在于强类型:导航属性在 CSDL 中显式声明了目标类型,HTTP API 中该属性名会直接成为可追加到资源 URL 的路径段,客户端无需额外信息即可访问关联资源,例如:

  • /user/{userId}/manager—— 多对一关系
  • /user/{userId}/messages—— 一对多关系

对比之下,传统的外键(foreign key)属性是弱类型机制:仅存放一个 id 值,客户端要遍历关系还需额外信息,关联资源的发现并不容易。而导航属性 + OData$expand查询参数可以让关联实体嵌套进主实体,一次往返同时取回双方数据。

三、实战示例:为 user 建模"银行账户信息"

原文档给出了一个完整的端到端示例:假设需要为user实体建模"银行账户信息",其中包含accountNumber(账号)与routingNumber(路由号)两个属性。

3.1 Don't:直接往 user 上挂属性(错误示范)

千万不要这样建模——这恰恰是核心类型限制要禁止的行为:

<EntityType name="user"> <Property Name="accountNumber" Type="Edm.string"/> <Property Name="routingNumber" Type="Edm.string"/> </EntityType>

这样做的后果是多方面的:user是跨工作负载共享的中心类型,每新增一个"便利字段"都会让所有消费方承受契约膨胀、语义混乱和潜在的破坏性变更风险。任何对user的改动都需要在 Microsoft Graph API review 中披露理由。

3.2 Do:新类型 + 导航属性(三种官方方案)

正确的做法是定义新实体类型bankAccountDetail,并在它与user之间建立导航关系。原文档给出三种可选形态:

方案一:核心类型上添加"包含式"导航属性(ContainsTarget="true")

先定义新实体类型:

<EntityType name="bankAccountDetail"> <Property Name="accountNumber" Type="Edm.string"/> <Property Name="routingNumber" Type="Edm.string"/> </EntityType>

再从user添加一个包含(contained)导航指向新类型:

<EntityType name="user"> <NavigationProperty Name="bankAccountDetail" Type="bankAccountDetail" ContainsTarget="true"/> </EntityType>

ContainsTarget="true"表示目标实体的生命周期由源实体控制、目标嵌入在源实体的 URL 空间内(例如/users/{id}/bankAccountDetail)。零或一对一、一对多(子资源从属父资源)的场景适合包含式关系。

方案二:新类型放入实体集,核心类型上添加导航属性

同样先定义bankAccountDetail实体类型:

<EntityType name="bankAccountDetail"> <Property Name="accountNumber" Type="Edm.string"/> <Property Name="routingNumber" Type="Edm.string"/> </EntityType>

将新类型放到独立的实体集或单例(singleton)中,使其可被独立寻址:

<EntitySet Name="bankAccountDetails" EntityType="bankAccountDetail">

再从user添加指向该类型(不包含)的导航属性:

<EntityType name="user"> <NavigationProperty Name="bankAccountDetail" Type="bankAccountDetail" /> </EntityType>

此方案中导航属性省略ContainsTarget,其默认值即为false(见 Navigation Property 模式 中关于ContainsTarget默认值的说明),表示非包含关系——目标实体的生命周期独立于user,例如银行账户本身可以独立存在、甚至被多个实体复用。

方案三:新类型放入实体集,导航属性放在新类型上

换一个方向:在bankAccountDetail上定义指向user的导航属性:

<EntityType name="bankAccountDetail"> <Property Name="accountNumber" Type="Edm.string"/> <Property Name="routingNumber" Type="Edm.string"/> <NavigationProperty Name="user" Type="microsoft.graph.user" /> </EntityType>

并同样把新类型放入实体集:

<EntitySet Name="bankAccountInformations" EntityType="bankAccountInformation">

(注:原文档此示例中实体集与实体类型的命名并不完全一致,实际建模时应保证EntitySet的EntityType指向已定义的类型名。)该方案把关系的"宿主"放在新类型上,适合"从银行账户反查用户"的访问模式,例如/bankAccountInformations/{id}/user。

3.3 三种方案的取舍依据

从 Navigation Property 模式 可以提炼出选择标准:

场景关系形态ContainsTarget推荐方案
银行账户生命周期完全依附于 user,无独立访问需求零或一对一 / 一对多true(必须包含)方案一
银行账户可独立存在、独立寻址,从 user 侧访问多对一 / 一对多(非包含)false(默认)方案二
访问主方向是"从账户反查 user"多对一false方案三

注意 Navigation Property 模式 中的关键约束:

  • 多对一关系总是非包含的,因为目标的生存期不能依赖源实体;
  • 零或一对一关系必须包含,可当作结构组织机制,把实体的部分属性拆分出去(类似复杂类型的用法),但当源与目标来自不同后端 API 时优先用导航属性而非复杂类型;
  • 一对多关系既可以是包含也可以是非包含,子资源上的父级导航属性(如parentInvoice)可用于expand单次往返取回父级数据,示例见GET /invoice/{invoiceId}/items/{itemId}?expand=parentInvoice(select=invoiceDate,Customer)。

四、导航属性的运行时语义:$ref、$expand 与绑定

既然替代方案以导航属性为核心,就需要理解它在 HTTP 层的完整语义(详见 Navigation Property 模式 的示例部分):

取回关联实体(导航属性默认不随实体返回,除非服务显式支持或用expand请求):

GET /users/{id}/manager?$select=id,displayName 200 OK Content-Type: application/json { "id": "6b3ee805-c449-46a8-aac8-8ff9cff5d213", "displayName": "Bob Boyce" }

仅取关联实体的引用($ref),适用于只想确认关联、不需要全部属性的场景:

GET /users/{id}/manager/$ref 200 OK Content-Type: application/json { "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/6b3ee805-c449-46a8-aac8-8ff9cff5d213/Microsoft.DirectoryServices.User" }

用 expand 一次取回双方:

GET /users/{id}?select=id,displayName&expand=manager(select=id,displayName) 200 OK Content-Type: application/json { "id": "3f057904-f936-4bf0-9fcc-c1e6f84289d8", "displayName": "Jim James", "manager": { "@odata.type": "#microsoft.graph.user", "id": "6b3ee805-c449-46a8-aac8-8ff9cff5d213", "displayName": "Bob Boyce" } }

创建/更新/清除关联:

POST /users Content-Type: application/json { "displayName": "Bob", "manager@odata.bind": "https://graph.microsoft.com/v1.0/users/{managerId}" } 201 Created
PATCH /users/{id} Content-Type: application/json { "displayName": "Bob", "manager@odata.bind": "https://graph.microsoft.com/v1.0/users/{managerId}" } 204 No Content
DELETE /users/{id}/manager/$ref 204 No Content

使用导航属性建模关系,相比弱类型外键带来的收益(原文档明确列出的强类型价值):

  • 支持自动生成文档与可视化;
  • 支持SDK 生成与客户端代码生成;
  • 服务端无需存储重复数据,跨 API 的数据一致性更好(重复数据无需定期刷新)。

五、配套建模模式:何时用新类型、用什么样的新类型

"创建新类型"并非单一形态,Microsoft Graph 提供了多种建模模式的组合选择(详见 GuidelinesGraph.md 资源建模模式章节):

  • 类型层次(Type hierarchy):一个抽象基类 + 每个变体一个子类型,见 subtypes.md。变体互斥、各有专属属性与行为时使用。典型如directoryObject→user/group/device的派生体系——device本身就是该层次的一个子类型。
  • Facets(切面):单一实体类型 + 每个变体一个复杂类型属性,见 facets.md。变体不互斥(如一个 driveItem 同时是文件又是图片)时使用。
  • Flat bag(属性平铺袋):单一类型容纳所有潜在属性 + 一个区分变体的type属性,见 flat-bag.md。仅适合少量变体、弱类型可接受的情形。
  • 集合子集(Collection subsets):以抽象基类 + 派生类型表达 All / None / 包含子集 / 排除子集,见 subsets.md。

三种主流模式的能力对比(出自 GuidelinesGraph.md):

API 特质 \ 模式属性与行为在元数据中描述支持属性/行为组合查询构建简单
类型层次是否否
Facets部分是是
Flat bag否否是

选择要点:层次模式把"哪些属性对哪些变体有效"的依赖完整固化在类型系统中,但过滤查询需要类型转换段(如$filter=microsoft.graph.user/jobTitle eq 'CEO');Facets 与 Flat bag 的$filter语法更简单。此外,Facets/Flat bag 往往需要大量可空属性,可空属性的使用边界见 nullable.md。

这些模式与核心类型限制是正交的:无论新类型采用层次、Facets 还是其他形态,其与user/group/device的关联都必须通过导航属性完成,而不能把结构属性直接塞进核心类型。

六、配套命名与契约约束

为核心类型建模新类型时,命名同样受 naming.md 规范约束,需特别注意与"新类型 + 导航属性"直接相关的条目:

  • 所有标识符(命名空间、entityTypes、entitySets、属性、动作、函数、枚举值)必须使用 lowerCamelCase;
  • 集合必须用复数名词命名(如bankAccountDetails),资源计数用名词 +Count后缀;
  • 标识符一律使用冗长命名,除领域主导缩写(如Url)外不得使用缩写;
  • 属性名应避免context、scope、resource这类在 API 领域被过度占用、失去含义的词;
  • 组合词中类型后缀加在末尾(如createdDateTime),避免aUser、theAccount、countOfBooks这类带冠词/介词的名字;
  • 对日期时间属性按DateTime/Date/Time后缀区分,身份属性使用字符串类型,外键用"关系名 +Id"(如subscriptionId)。

从契约演进角度看,在 GuidelinesGraph.md 的 API contract 章节 中,"向既有类型添加非空属性(Nullable="false")"被明确列为破坏性变更,而"添加可空或有默认值的属性"是非破坏性的。这正是核心类型限制的底层逻辑之一:对user这类被全生态共享的类型,任何属性追加都会扩散到所有消费方,因此更要走"新类型 + 导航"的隔离式演进路线。

七、总结

围绕 Microsoft Graph 的核心类型建模,可以收敛为三条可执行规则:

  1. 默认不扩展:user、group、device是核心类型,向它们添加结构属性需要强有力的论证,且仅当属性对实体本身固有时才被允许。
  2. 优先新类型 + 导航:需要表达依附于核心实体却非其固有属性的信息时,先创建新类型,再按访问模式选择——核心类型上包含式导航(方案一)、核心类型上非包含导航(方案二)、新类型上反向导航(方案三)。
  3. 守契约、用强类型:借助导航属性的强类型机制获得 SDK 生成、文档自动化和数据一致性收益,同时遵循 命名规范 与 破坏性变更约束,让 API 可持续演化。

相关规范文本可在仓库中继续深读:核心类型专文、总纲中的限制章节、导航属性模式、以及与之配合的 类型层次、Facets、Flat Bag 和 集合子集 模式文档。

  • API设计

【免费下载链接】api-guidelines

Microsoft REST API Guidelines

项目地址:https://gitcode.com/gh_mirrors/ap/api-guidelines
点击查看免费下载
上一篇:Windows HEIC缩略图预览完整指南:让iPhone照片在Windows完美显示
下一篇:Windows HEIC缩略图终极解决方案:如何让Windows资源管理器完美显示iPhone照片预览

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 9:16:41

软件测试论文参考文献全攻略:从检索到引用一步到位

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 9:15:38

YOLOv8+ByteTrack多目标车辆实时检测与流量统计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 9:15:20

Keil软件仿真:从配置到结构体、堆栈与R6002排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 9:14:27

用C++解决数独问题

前言数独&#xff08;Sudoku&#xff09;问题很适合当作算法与 C 语言特性的综合练习&#xff1a;问题规模小到可以在一瞬间求解&#xff0c;规则又足够结构化&#xff0c;能清楚地看到"建模—剪枝—搜索"这条主线。它的标准形式是一个 99 的格子&#xff0c;要求每行…

作者头像 李华
网站建设 2026/10/1 9:13:16

水稻稻穗检测为何首选YOLOv8?小目标、高密度、田间部署实战指南

简介&#xff1a;本资源是一套专为农业AI视觉检测任务设计的YOLO格式水稻稻穗检测数据集&#xff0c;面向计算机视觉初学者、农业智能化研究者及YOLO模型实践者&#xff0c;解决稻穗目标在复杂田间场景下的精准定位与识别问题。数据集严格遵循YOLOv5目录结构&#xff0c;含训练…

作者头像 李华
网站建设 2026/10/1 9:10:57

轻量级车道线检测模型:Python实现与工程落地指南

简介&#xff1a;本资源是一套基于Python实现的轻量级车道线检测模型源码及配套文档&#xff0c;面向计算机视觉初学者、智能交通系统开发者及自动驾驶算法实践者&#xff0c;聚焦于在精度可控前提下显著提升检测效率的实际需求。资源包共11个文件&#xff0c;含4个核心Python脚…

作者头像 李华