7.2 Organization Service Operations, Concurrency, Transactions & Bulk Operations
Key Takeaways
- IOrganizationService exposes Create, Retrieve, RetrieveMultiple, Update, Delete, and Execute; Execute handles every non-CRUD message including custom APIs.
- RetrieveMultiple accepts a strongly-typed QueryExpression or a FetchExpression wrapping FetchXML; both hit the same query engine.
- ExecuteMultipleRequest allows partial success via ContinueOnError; ExecuteTransactionRequest guarantees all-or-nothing atomicity across a batch of requests.
- CreateMultipleRequest and UpdateMultipleRequest are bulk, single-table messages that outperform looping individual Create/Update calls.
- Optimistic concurrency uses RowVersion with ConcurrencyBehavior.IfRowVersionMatches to prevent one client from silently overwriting another's update.
The Organization service is the .NET SDK's counterpart to the Web API: a strongly-typed (or late-bound) programming surface used from plug-ins, custom workflow activities, console applications, and Azure Functions written in C#. PL-400 expects developers to know its core operations, how it compares to the Web API for querying, and how to make bulk and concurrent operations both correct and efficient.
The IOrganizationService Interface
Every interaction with Dataverse from server-side .NET code flows through IOrganizationService, which exposes five core methods:
Create(Entity)— inserts a record, returns the newGuidRetrieve(entityName, id, ColumnSet)— retrieves a single record by primary keyRetrieveMultiple(QueryBase)— retrieves a set of records using aQueryExpression,FetchExpression, orQueryByAttributeUpdate(Entity)— updates only the attributes set on theEntityobject (a partial update, like the Web API'sPATCH)Delete(entityName, id)— deletes a recordExecute(OrganizationRequest)— the catch-all for every message that isn't plain CRUD: custom APIs,ExecuteMultipleRequest,AssignRequest, and dozens of built-in messages
Records can be represented as early-bound typed classes (generated from the schema, giving compile-time checking and IntelliSense) or as late-bound generic Entity objects (looked up by logical name string at runtime, more flexible but with no compile-time safety).
Query Expression vs. FetchXML
RetrieveMultiple accepts either a QueryExpression — a strongly-typed, fluent C# object model built with .AddColumns(), .Criteria.AddCondition(), and .AddLink() for joins — or a FetchExpression wrapping a FetchXML string. QueryExpression is easier to build and refactor programmatically and catches many mistakes at compile time; FetchXML is more expressive for aggregation, grouping, and complex multi-entity joins, and is the format used by views, so it is often copied directly from an exported view definition. Both ultimately execute against the same query engine — the choice is about developer ergonomics and query complexity, not capability differences for simple filters.
Bulk and Transactional Operations
Looping over individual Create/Update calls does not scale and does not give atomicity. Three SDK requests address this:
| Request | Behavior | Use when |
|---|---|---|
ExecuteMultipleRequest | Sends many requests in one call; ContinueOnError controls whether one failure stops the batch; ReturnResponses controls whether individual results come back | Bulk operations where partial success is acceptable |
ExecuteTransactionRequest | Sends many requests in one call that must all succeed or all roll back together | Bulk operations that must be atomic — no partial writes |
CreateMultipleRequest / UpdateMultipleRequest | Bulk messages for a single table that push many records to the platform in one operation, generally faster than looping | High-volume same-table inserts/updates where elastic or standard table bulk throughput matters |
ExecuteMultipleRequest with ContinueOnError=true is the wrong choice whenever a scenario needs all-or-nothing semantics — that requirement points to ExecuteTransactionRequest instead, a common exam distinction.
Concurrency Control
Two clients retrieving the same record and both saving changes can silently overwrite each other's work — the classic lost update problem. Dataverse supports optimistic concurrency using the record's RowVersion value: a client retrieves the record (which includes its current RowVersion), and when submitting the Update, sets request.ConcurrencyBehavior = ConcurrencyBehavior.IfRowVersionMatches. If another process modified the row in between, the row version no longer matches and the update fails with a concurrency exception rather than silently clobbering the other change. This is far more scalable than pessimistic row locking, which the platform does not expose to custom code for the duration of a client session.
Performance Considerations
For both querying and writing, PL-400 scenario questions reward developers who: request only needed columns (ColumnSet with explicit column names, never ColumnSet(true) in production code); page large result sets rather than pulling everything into memory; prefer bulk/batch messages over per-record loops; and choose ExecuteTransactionRequest deliberately, since wrapping unrelated operations in a transaction unnecessarily increases the chance of the whole batch failing over one unrelated row.
Web API or Organization Service — Which One?
Both surfaces expose the same underlying platform, so the choice is mostly about the calling context rather than capability:
| Consideration | Favors Web API | Favors Organization Service |
|---|---|---|
| Language/runtime | Any language that can make HTTP calls (JavaScript, Python, Node, PowerShell) | .NET languages (C#, VB.NET) |
| Execution location | Client scripts, PCF components, canvas apps, external web/mobile apps, Azure Functions written in any stack | Plug-ins, custom workflow activities, .NET console apps and services |
| Typing | JSON, dynamically shaped | Early-bound generated classes available for compile-time safety |
| Query style | OData query options, with FetchXML as a fallback | QueryExpression natively, or FetchXML via FetchExpression |
Inside a plug-in specifically, the Organization service is the only option — plug-ins receive an IOrganizationService instance from the execution context and are not expected to make raw Web API HTTP calls to their own environment. Outside the sandbox (Azure Functions, console apps, external services), the choice comes down to the language and tooling already in use.
Executing a Custom API from Code
Calling a registered custom API through the Organization service uses the generic Execute method with an OrganizationRequest built from the API's unique name and its defined input parameters:
var request = new OrganizationRequest("new_CalculateDiscount")
{
["AccountId"] = accountId,
["OrderTotal"] = orderTotal
};
var response = service.Execute(request);
var discount = (decimal)response["DiscountAmount"];
This pattern — building a generic OrganizationRequest by message name rather than calling a dedicated strongly-typed method — is exactly how custom APIs and custom actions are invoked from .NET code, mirroring how the Web API calls the same custom API as a bound or unbound action.
A developer must create 50 related records where either all 50 succeed or none are committed. Which SDK request satisfies this requirement?
How does the Organization service prevent one client from silently overwriting a change another client just made to the same record?