9.4 Upsert-Based Synchronization with the UpsertRequest Message
Key Takeaways
- Upsert combines Create and Update into a single idempotent operation: it updates the record if a match is found by primary key or alternate key, and creates a new record if not.
- The SDK's UpsertRequest.Target uses Entity.KeyAttributes to identify the record by alternate key; the response's RecordCreated property reports whether a create or update actually happened.
- In the Web API, a PATCH request to an alternate-key URL is treated as upsert by default; adding If-Match: * forces update-only, and If-None-Match: * forces create-only.
- Don't include the alternate key's own column values in the request body — Dataverse strips or copies them automatically, and mismatches between the URL key and the body risk unexpected data.
- Elastic tables apply Upsert directly without firing separate Create/Update events, so business logic tied to those events must also be duplicated into any Upsert-handling logic.
The previous section covered how a batch integration finds out what changed and which record it corresponds to. This section covers the write side: once you know a piece of external data needs to land in Dataverse, how do you save it without first checking whether the record already exists?
The Problem Upsert Solves
A naive integration loop would retrieve the record by its alternate key, check whether anything came back, and then branch to either Create or Update. That's an extra round trip for every single record, on top of whatever the actual write costs — expensive at scale and easy to get wrong under concurrent writes. The Upsert message collapses that into one call: Upsert = Update-if-it-exists, Create-if-it-doesn't, decided entirely on the server side.
How UpsertRequest Works (SDK)
Entity target = new Entity("account", "accountnumber", "A123");
target["name"] = "Contoso West Region";
var request = new UpsertRequest { Target = target };
var response = (UpsertResponse)service.Execute(request);
if (response.RecordCreated)
Console.WriteLine("New record created.");
else
Console.WriteLine("Existing record updated.");
The Target entity typically identifies the row through Entity.KeyAttributes (an alternate key) rather than the primary GUID, since that's the whole point of an integration scenario — the caller usually doesn't know the GUID. On the server:
- If a matching record is found (by primary key or alternate key), Dataverse strips any attributes that duplicate the key values, calls
Update, and setsUpsertResponse.RecordCreatedtofalse. - If no match is found, Dataverse copies the key attribute values into the entity's attribute collection (so they're saved as real column values, not just match criteria), calls
Create, and setsRecordCreatedtotrue.
UpsertResponse.Target always returns an EntityReference to the affected record either way, and RecordCreated is the one property that lets your integration branch its own logic — for example, kicking off a "new customer" onboarding flow only when RecordCreated is true, versus a "customer updated" notification otherwise.
How Upsert Works (Web API)
The Web API doesn't have a separate Upsert verb — it overloads PATCH:
PATCHto an alternate-key URL with no extra header → treated as upsert; creates the record if the key doesn't match anything, updates it if it does. Either way the response is204 No Content, so the status code alone doesn't tell you which happened.PATCHwith anIf-Match: *header → forces update-only. If no record matches the key, the operation fails rather than creating one.PATCHwith anIf-None-Match: *header → forces create-only, blocking an update if a matching record already exists.- Add
Prefer: return=representationto get a distinguishable response:201 Createdfor a new record,200 OKfor an updated one — at the cost of an extra internalRetrieve, so scope the$selectdown to just the primary key if you use it.
| Header combination | Behavior |
|---|---|
| None | Upsert: create or update, whichever applies |
If-Match: * | Update only — fails if the record doesn't exist |
If-None-Match: * | Create only — fails if the record already exists |
Prefer: return=representation | Adds 201/200 distinction to any of the above, at a performance cost |
Practical Guidance
Don't put the alternate key's own column values in the request body — for the Web API that means not repeating them in the JSON payload, and for the SDK it means not duplicating them in Entity.Attributes. On an update, the server discards any attribute data that overlaps with the key columns anyway; the risk is on create, where mismatched values between the URL/KeyAttributes and the body could produce a record whose key doesn't match what you expect.
Elastic tables behave differently. For a standard table, Upsert internally calls the real Create or Update message, so any plug-ins registered on those events still fire. For an elastic table, Upsert applies the change directly — there's no separate Create or Update event raised. If business logic needs to run on every insert or every change to an elastic table, that logic has to be included in whatever handles the Upsert path too, not just registered against Create/Update.
When not to use it: if you're certain the record doesn't already exist — for example, a first-time bulk load into an empty table — plain Create is cheaper, since Upsert carries a performance penalty from the extra existence check. Save Upsert for the case it's built for: an ongoing sync job, most often paired with the change-tracking delta and alternate-key lookup from the previous section, where you genuinely don't know ahead of time whether each incoming record is new or already there.
A nightly integration job calls UpsertRequest for a batch of records identified by an alternate key. For records that already exist in Dataverse, what does the server do before calling Update?
A developer sends a Web API PATCH request to an alternate-key URL but must guarantee the request fails rather than create a new record if no match exists. Which header should be included?