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.
Last updated: July 2026

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 new Guid
  • Retrieve(entityName, id, ColumnSet) — retrieves a single record by primary key
  • RetrieveMultiple(QueryBase) — retrieves a set of records using a QueryExpression, FetchExpression, or QueryByAttribute
  • Update(Entity) — updates only the attributes set on the Entity object (a partial update, like the Web API's PATCH)
  • Delete(entityName, id) — deletes a record
  • Execute(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:

RequestBehaviorUse when
ExecuteMultipleRequestSends many requests in one call; ContinueOnError controls whether one failure stops the batch; ReturnResponses controls whether individual results come backBulk operations where partial success is acceptable
ExecuteTransactionRequestSends many requests in one call that must all succeed or all roll back togetherBulk operations that must be atomic — no partial writes
CreateMultipleRequest / UpdateMultipleRequestBulk messages for a single table that push many records to the platform in one operation, generally faster than loopingHigh-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:

ConsiderationFavors Web APIFavors Organization Service
Language/runtimeAny language that can make HTTP calls (JavaScript, Python, Node, PowerShell).NET languages (C#, VB.NET)
Execution locationClient scripts, PCF components, canvas apps, external web/mobile apps, Azure Functions written in any stackPlug-ins, custom workflow activities, .NET console apps and services
TypingJSON, dynamically shapedEarly-bound generated classes available for compile-time safety
Query styleOData query options, with FetchXML as a fallbackQueryExpression 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.

Test Your Knowledge

A developer must create 50 related records where either all 50 succeed or none are committed. Which SDK request satisfies this requirement?

A
B
C
D
Test Your Knowledge

How does the Organization service prevent one client from silently overwriting a change another client just made to the same record?

A
B
C
D