The Dataverse SDK gives you two ways to talk to the platform: the .NET-based Organization Service and the OData Web API. Both wrap the same underlying engine, but they differ in latency, payload size, transaction semantics, and where they can run. Choose per workload, not per team preference.
Use the Organization Service for plugins
In-process plugin code should never use the Web API. The Organization Service exposes the existing transaction context, so a plugin’s Create call participates in the same database transaction as the triggering operation. Use the Web API and you spawn an HTTP round trip, often back to the same server, with no transaction sharing. You also lose impersonation context that the pipeline already provides.
var service = (IOrganizationService)serviceProvider
.GetService(typeof(IOrganizationService));
service.Create(new Entity("contact") { ["lastname"] = "Smith" });
Stick with this pattern in every step.
Use the Web API for external clients
Anything outside Dataverse should use the Web API. The reasons:
- No SDK assemblies to manage; just HTTPS and OAuth
- First-class support in JavaScript, Python, Go, Postman, Azure Functions
- Bearer token auth via Microsoft Entra; no proxy gymnastics
- Standard OData query syntax that any developer can read
GET https://contoso.crm.dynamics.com/api/data/v9.2/contacts?$select=fullname,emailaddress1&$filter=statecode eq 0&$top=50
Authorization: Bearer {token}
The Organization Service still works from external clients, but you carry SDK dependencies for marginal benefit.
Batch decisions
For external workloads, the Web API $batch endpoint groups up to 1000 requests into a single HTTP call with optional change set transactionality. This often beats individual Organization Service calls because of network round trip elimination.
Inside a plugin, ExecuteMultipleRequest is the equivalent. It does not give you a single transaction by default; set ContinueOnError = false and ReturnResponses = true and remember each operation is still its own transaction unless wrapped in ExecuteTransactionRequest.
Pagination and large reads
The Web API exposes @odata.nextLink for paging. Page size is server-controlled; ask politely with the Prefer: odata.maxpagesize=500 header but expect Dataverse to cap at 5000.
The Organization Service uses PagingInfo with cookies. For reads above 5000 records, paging cookies outperform $skip because they avoid full scan re-evaluation.
Authentication latency
Organization Service via the .NET SDK caches connection state, so subsequent calls are fast. The Web API requires a valid bearer token. Cache tokens for their full lifetime (typically 60 to 90 minutes) using MSAL token cache; do not request a new token per call. Token acquisition is 200 to 500 ms, which can dominate small-payload latency if you skip caching.
Long-running operations
Both surfaces respect the 2-minute synchronous limit. For longer work, use the Web API’s Prefer: respond-async header to get a 202 with a status URL. Poll the status URL until the operation completes. The Organization Service has no async equivalent; you must hand off to a System Job.
When to mix
A common pattern: external service writes via Web API for simplicity, internal plugin reads via Organization Service for transaction integrity. Both are fine. The mistake is using the Web API inside a plugin to call back into the same Dataverse instance.
What to do this week: audit your plugin codebase for any HttpClient calls hitting your Dataverse Web API. Replace each with Organization Service calls and measure the latency drop.