- API设计
【免费下载链接】api-guidelines
Microsoft REST 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 中,被正式认定为核心类型的只有三个:
usergroupdevice
这三类实体在 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上加结构属性,正确的做法是:
- 创建一个新类型,把拟新增结构属性所承载的信息建模进去;
- 在核心类型与新类型之间用导航属性(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 CreatedPATCH /users/{id} Content-Type: application/json { "displayName": "Bob", "manager@odata.bind": "https://graph.microsoft.com/v1.0/users/{managerId}" } 204 No ContentDELETE /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 的核心类型建模,可以收敛为三条可执行规则:
- 默认不扩展:
user、group、device是核心类型,向它们添加结构属性需要强有力的论证,且仅当属性对实体本身固有时才被允许。 - 优先新类型 + 导航:需要表达依附于核心实体却非其固有属性的信息时,先创建新类型,再按访问模式选择——核心类型上包含式导航(方案一)、核心类型上非包含导航(方案二)、新类型上反向导航(方案三)。
- 守契约、用强类型:借助导航属性的强类型机制获得 SDK 生成、文档自动化和数据一致性收益,同时遵循 命名规范 与 破坏性变更约束,让 API 可持续演化。
相关规范文本可在仓库中继续深读:核心类型专文、总纲中的限制章节、导航属性模式、以及与之配合的 类型层次、Facets、Flat Bag 和 集合子集 模式文档。
- API设计
【免费下载链接】api-guidelines
Microsoft REST API Guidelines
相关推荐
Livewire Synthesizer 深入指南:为 Laravel 组件属性扩展任意数据类型支持
Livewire Synthesizer 深入指南:为 Laravel 组件属性扩展任意数据类型支持 导读 Synthesizer(合成器)是 Livewire
后端前端在 Meteor 中启用核心包 TypeScript 类型:zodern:types 与 using-core-types 完整指南
在 Meteor 中启用核心包 TypeScript 类型:zodern:types 与 using core types 完整指南 导读 本指南围绕 Mete
后端前端开发工具移动开发React属性类型检查终极指南:深入理解prop-types库的核心机制
React属性类型检查终极指南:深入理解prop types库的核心机制 prop types是React生态中用于运行时类型检查的核心库,它能够帮助开发者在开
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考