Microsoft Graph API Design Pattern
The Upsert
pattern is a non-destructive idempotent operation using a client-provided key, that ensures that system resources can be deployed in a reliable, repeatable, and controlled way, typically used in Infrastructure as Code (IaC) scenarios.
Infrastructure as code (IaC) defines system resources and topologies in a declarative manner that allows teams to manage those resources as they would code. Practicing IaC helps teams deploy system resources in a reliable, repeatable, and controlled way. IaC also helps automate deployment and reduces the risk of human error, especially for complex large environments. Customers want to adopt IaC practices for many of the resources managed through Microsoft Graph.
Most resources' creation operations in Microsoft Graph are not idempotent in nature. As a consequence, API consumers that want to offer IaC solutions, must create compensation layers that can mimic idempotent behavior. For example, when creating a resource, the compensation layer must check whether the resource first exists, before trying to create or update the resource.
Additionally, IaC code scripts or templates usually employ client-provided names (or keys) to track resources in a predictable manner, whereas Microsoft Graph guidelines suggests use of POST
to create new entities with service-generated keys.
The solution is to use an Upsert
pattern, to solve for the non-idempotent creation and client-provided naming problems.
Upsert
usesPATCH
with a client-provided key in the URL:- If there is a natural client-provided key that can serve as the primary key, then the service should support
Upsert
with that key. - If the primary key is service-generated, the client-provided key should use an alternate key to support idempotent creation.
- For a non-existent resource (specified by the client-provided key) the service must handle this as a "create" (aka insert). As part of creation, the service must still generate the primary key value, if appropriate.
- For an existing resource (specified by the client-provided key) the service must handle this as an "update".
- If there is a natural client-provided key that can serve as the primary key, then the service should support
- If using an alternate key, then
- for IaC scenarios, the alternate key should be called
uniqueName
, if there isn't already a more natural existing property that could be used as an alternate key. - the service must also support
GET
using the alternate key pattern.
- for IaC scenarios, the alternate key should be called
- Services should always support
POST
to the collection URL.- For service-generated keys, this should return the server generated key.
- For client-provided keys, the client can provide the key as part of the request payload.
- If a service does not support
Upsert
, then aPATCH
call against a non-existent resource must result in an HTTP "404 not found" error.
This solution allows for existing resources that follow Microsoft Graph conventions for CRUD operations to add Upsert
without impacting existing apps or functionality.
Ideally, all new entity types should support an Upsert
mechanism, especially where they support control-plane APIs, or are used in admin style or IaC scenarios.
This pattern should be adopted for resources that are managed through infrastructure as code or desired state configuration.
- Services with existing APIs that use a client-defined key that want to start supporting the
Upsert
pattern may have concerns about backwards compatibility. API producers can require clients to opt-in to theUpsert
pattern, by using thePrefer: create-if-missing
HTTP request header. Upsert
can also be supported against singletons, using aPATCH
to the singleton's URL.- Services that support
Upsert
should allow clients to use the:If-Match=*
request header to explicitly treat anUpsert
request as an update and not an insert.If-None-Match=*
request header to explicitly treat anUpsert
request as an insert and not an update.
- The client-provided alternate key must be immutable after being set. If its value is null then it should be settable as a way to backfill existing resources for use in IaC scenarios.
- API producers could use
PUT
operations to create or update, but generally this approach is not recommended due to the destructive nature ofPUT
's replace semantics. - API producers may annotate entity sets, singletons and collections to indicate that entities can be "upserted". The example below shows this annotation for the
groups
entity set.
<EntitySet Name="groups" EntityType="microsoft.graph.group">
<Annotation Term="Org.OData.Capabilities.V1.UpdateRestrictions">
<Record>
<PropertyValue Property="Upsertable" Bool="true"/>
</Record>
</Annotation>
</EntitySet>
For these examples we'll use the group
entity type, which defines both a primary (service-generated) key (id
) and an alternate (client-provided) key (uniqueName
).
<EntityType Name="group">
<Key>
<PropertyRef Name="id"/>
</Key>
<Property Name="id" Type="Edm.String"/>
<Property Name="uniqueName" Type="Edm.String"/>
<Property Name="displayName" Type="Edm.String"/>
<Property Name="description" Type="Edm.String"/>
<Annotation Term="Org.OData.Core.V1.AlternateKeys">
<Collection>
<Record Type="Org.OData.Core.V1.AlternateKey">
<PropertyValue Property="Key">
<Collection>
<Record Type="Org.OData.Core.V1.PropertyRef">
<PropertyValue Property="Name" PropertyPath="uniqueName" />
</Record>
</Collection>
</PropertyValue>
</Record>
</Collection>
</Annotation>
</Property>
</EntityType>
Create a new group, with a uniqueName
of "Group157". In this case, this group does not exist.
PATCH /groups(uniqueName='Group157')
Prefer: return=representation
{
"displayName": "My favorite group",
"description": "All my favorite people in the world"
}
Response:
201 created
Preference-Applied: return=representation
{
"id": "1a89ade6-9f59-4fea-a139-23f84e3aef66",
"displayName": "My favorite group",
"description": "All my favorite people in the world",
"uniqueName": "Group157"
}
Create a new group, with a uniqueName
of "Group157", exactly like before. Except in this case, this group already exists. This is a common scenario in IaC, when a deployment template is re-run multiple times.
PATCH /groups(uniqueName='Group157')
Prefer: return=representation
{
"displayName": "My favorite group",
"description": "All my favorite people in the world"
}
Response:
200 ok
Preference-Applied: return=representation
{
"id": "1a89ade6-9f59-4fea-a139-23f84e3aef66",
"displayName": "My favorite group",
"description": "All my favorite people in the world",
"uniqueName": "Group157"
}
Notice how this operation is idempotent in nature, rather than returning a 409 conflict error.
Update "Group157" group with a new description.
PATCH /groups(uniqueName='Group157')
Prefer: return=representation
{
"description": "Some of my favorite people in the world."
}
Response:
200 ok
Preference-Applied: return=representation
{
"id": "1a89ade6-9f59-4fea-a139-23f84e3aef66",
"displayName": "My favorite group",
"description": "Some of my favorite people in the world.",
"uniqueName": "Group157"
}
In this case, the group API is a pre-existing API that supports PATCH
with a client-provided alternate key. To enable Upsert
behavior,
the client must opt-in using an HTTP request header, to create a new group using PATCH
.
PATCH /groups(uniqueName='Group157')
Prefer: create-if-missing; return=representation
{
"displayName": "My favorite group",
"description": "All my favorite people in the world"
}
Response:
201 created
Preference-Applied: create-if-missing; return=representation
{
"id": "1a89ade6-9f59-4fea-a139-23f84e3aef66",
"displayName": "My favorite group",
"description": "All my favorite people in the world",
"uniqueName": "Group157"
}
Following on from the last example, the same request to create a new group, with a uniqueName
of "Group157",
without the opt-in header, results in a 404 HTTP response code.
PATCH /groups(uniqueName='Group157')
Prefer: return=representation
{
"displayName": "My favorite group",
"description": "All my favorite people in the world"
}
Response:
404 not found