diff --git a/graph/GuidelinesGraph.md b/graph/GuidelinesGraph.md index 1aa99b62..27c97c9a 100644 --- a/graph/GuidelinesGraph.md +++ b/graph/GuidelinesGraph.md @@ -202,13 +202,13 @@ The three most often used patterns in Microsoft Graph today are type hierarchy, - **[Enums](./patterns/enums.md)** represent a subset of the nominal type they rely on, and are especially useful in cases where certain properties have predefined, limited options. -The following table shows a summary of the main qualities for each pattern and can help you select a pattern fit for your use case. +The following table shows a summary of the main qualities for each pattern and can help you select a pattern fit for your use case. The **TypeSpec representation** column shows how each pattern is authored with the [`@microsoft/typespec-msgraph`](https://aka.ms/typespec) library; see the [facets TypeSpec example](./patterns/facets.md#facets-in-typespec) for a worked sample. -| API qualities\patterns | Properties and behavior described in metadata | Supports combinations of properties and behaviors | Simple query construction | -|-------------------------|-----------------------------------------------|---------------------------------------------------|---------------------------| -| Type hierarchy | yes | no | no | -| Facets | partially | yes | yes | -| Flat bag | no | no | yes | +| API qualities\patterns | Properties and behavior described in metadata | Supports combinations of properties and behaviors | Simple query construction | TypeSpec representation | +|-------------------------|-----------------------------------------------|---------------------------------------------------|---------------------------|-------------------------| +| Type hierarchy | yes | no | no | `@abstract` + `extends` | +| Facets | partially | yes | yes | `@facet` on a nullable `@complex` property | +| Flat bag | no | no | yes | `@flatBag` + `@variant` | #### Pros and cons diff --git a/graph/patterns/facets.md b/graph/patterns/facets.md index 949d573a..d69a2790 100644 --- a/graph/patterns/facets.md +++ b/graph/patterns/facets.md @@ -107,3 +107,98 @@ Response shortened for readability: } ] ``` + +## Facets in TypeSpec + +In [TypeSpec](https://aka.ms/typespec), the facets pattern is expressed with the `@facet` decorator from the `@microsoft/typespec-msgraph` library. A facet is a **nullable property** on an `@entity` model whose type is a `@complex` model. The `@facet` decorator marks the property as a variant facet so that authoring tooling and linters can validate it. + +The following TypeSpec models the same `driveItem` facets shown in the CSDL example above — each variant (`audio`, `file`, `folder`, `image`, `video`) is a complex type, and the entity carries one nullable facet property per variant: + +```typespec +// One @complex type per variant facet +@complex +model audio { + @doc("The title of the album for this audio file.") + album: string | null; +} + +@complex +model file { + @doc("The MIME type for the file.") + mimeType: string | null; +} + +@complex +model folder { + @doc("Number of children contained immediately within this container.") + childCount: int32 | null; +} + +@complex +model image { + @doc("Width of the image, in pixels.") + width: int32 | null; + + @doc("Height of the image, in pixels.") + height: int32 | null; +} + +@complex +model video { + @doc("Duration of the video, in milliseconds.") + duration: int64 | null; +} + +// The entity declares one nullable facet property per variant +@entity +model driveItem { + @key id: string; + + @doc("The name of the item (file name, folder name).") + displayName: string | null; + + @doc("Audio facet. Present when the item is an audio file.") + @facet audio: audio | null; + + @doc("File facet. Present when the item is a file.") + @facet file: file | null; + + @doc("Folder facet. Present when the item is a folder.") + @facet folder: folder | null; + + @doc("Image facet. Present when the item is an image.") + @facet image: image | null; + + @doc("Video facet. Present when the item is a video.") + @facet video: video | null; +} +``` + +### Rules for `@facet` + +The `@facet` decorator is validated by the `@microsoft/typespec-msgraph` linter: + +- The property type must be a `@complex` model — primitive, enum, and entity types are rejected. +- The property must be **nullable** (`T | null`) — a variant may be absent. +- The property must be declared on an `@entity` model — facets on `@complex` models are rejected. + +### Compiled CSDL + +`@facet` is an **authoring-time marker**: it carries no wire-format meaning, so a facet property compiles to a standard nullable complex-typed property — identical to what a hand-authored facet produces. The TypeSpec above emits: + +```xml + + + + + + + + + + + + +``` + +Because `@facet` adds no CSDL annotation, applying or omitting it produces byte-identical metadata; its value is the design intent it records and the linter checks it enables. diff --git a/graph/patterns/flat-bag.md b/graph/patterns/flat-bag.md index 5dedbeef..5e38b871 100644 --- a/graph/patterns/flat-bag.md +++ b/graph/patterns/flat-bag.md @@ -48,4 +48,52 @@ The recurrencePattern has six variants expressed as six different values of the -``` \ No newline at end of file +``` + +### TypeSpec representation + +The same `recurrencePattern` flat-bag is expressed in TypeSpec using the `@flatBag` decorator on the model plus `@variant` decorators on the variant-conditional sibling properties. +The discriminator property is conventionally named `type`, and the discriminator enum must be closed (no `unknownFutureValue`). + +```TypeSpec +enum recurrencePatternType { + daily: 0, + weekly: 1, + absoluteMonthly: 2, + relativeMonthly: 3, + absoluteYearly: 4, + relativeYearly: 5, +} + +@flatBag("type") +@complex model recurrencePattern { + type: recurrencePatternType | null; + + // Shared property — meaningful for every variant + interval: int32; + + // wire-fidelity: shipped as Nullable="false"; variant-conditional but non-nullable + @variant("absoluteMonthly", "absoluteYearly") dayOfMonth: int32; + + @variant("weekly", "relativeMonthly", "relativeYearly") daysOfWeek: dayOfWeek[]; + + @variant("weekly") firstDayOfWeek: dayOfWeek | null; + + @variant("relativeMonthly", "relativeYearly") index: weekIndex | null; + + // wire-fidelity: shipped as Nullable="false"; variant-conditional but non-nullable + @variant("absoluteYearly", "relativeYearly") month: int32; +} +``` + +The `@flatBag` and `@variant` decorators are pure semantic metadata — they do not change the compiled CSDL output, which matches the CSDL shown above. +Their purpose is to enable validation that variant-conditional sibling properties target valid discriminator enum members, and to surface the pattern to downstream tooling. + +**Authoring rules:** + +- The discriminator property is typed as ` | null` to match the shipped Graph CSDL convention. +- Variant-conditional non-collection siblings are nullable (`T | null`). +- Variant-conditional collection siblings use `T[]` rather than `T[] | null` — empty array signals absence per Graph wire convention. +- Properties meaningful for every variant (`interval` above) remain unannotated. +- Non-nullable variant-conditional siblings (like `dayOfMonth`, `month`) preserve the shipped Graph wire shape. + The inline `// wire-fidelity:` comment documents the exception, and a `#suppress` for `flat-bag-siblings-should-be-nullable` may be needed under stricter rulesets. \ No newline at end of file diff --git a/graph/patterns/subtypes.md b/graph/patterns/subtypes.md index aad10593..3e281b4e 100644 --- a/graph/patterns/subtypes.md +++ b/graph/patterns/subtypes.md @@ -70,6 +70,47 @@ Groups and users are derived types and modeled as follows: ``` +### TypeSpec representation + +The same `directoryObject` hierarchy is expressed in TypeSpec using the `@abstract` decorator on the base type and the native `extends` keyword on each derived type. +No purpose-built decorators are needed beyond `@abstract`, `@entity`, and `@key`. + +```TypeSpec +@abstract +@entity model entity { + @key id: string; +} + +@abstract +@entity model directoryObject extends entity { + deletedDateTime: utcDateTime | null; +} + +@entity model group extends directoryObject { + description: string | null; + // ... other group-specific properties +} + +@entity model user extends directoryObject { + jobTitle: string | null; + // ... other user-specific properties +} +``` + +The `@abstract` decorator emits `Abstract="true"` in CSDL and marks the type as non-instantiable — consumers must POST or PATCH against a derived type. +Each derived type inherits the base properties (including the key) via `extends` and adds only its own variant-specific properties. +The TypeSpec compiler rejects redeclaration of base properties on derived types. + +**Authoring rules:** + +- The base type carries `@abstract` and the `@key` property; derived types inherit both via `extends`. +- Derived types use `extends ` and add only their own variant-specific properties. +- Derived types do NOT redeclare base properties (compile error). +- Hierarchy depth ≤ 3 levels — deeper hierarchies usually indicate wrong modeling. + +The compiled CSDL matches the structure shown above. +URL and query semantics shown in the following sections apply regardless of authoring language. + An API request to get members of a group returns a heterogeneous collection of users and groups where each element can be a user or a group, and has an additional `@odata.type` property that specifies the subtype: