Perform operations by using the Organization service
The previous unit closed with a pointer toward the Organization service, "a strongly typed alternative built for .NET developers." This unit picks up that thread: you move from composing raw Web API requests to working with the Organization service's message-based request and response pattern. That way, you can reason about when a strongly typed, compiled approach serves an integration better than HTTP calls.
Meet the coordinator approval service
Contoso's fulfillment coordinator doesn't work through the employee portal or the external inventory service you saw in the previous unit. Instead, the coordinator's team owns a compiled .NET background service that processes pending contoso_request rows on a schedule, checks each one against the requesting employee's contoso_allowance, and moves the row's status forward or back depending on the outcome.
Unlike the portal and the inventory service, this approval service is trusted, long-running, and written entirely in C#. Its logic depends on knowing table and column names before the code ever runs, not on discovering them at request time.
That distinction matters because this trade-off is exactly what shapes the Organization service. Where the Web API lets any HTTP-capable client read and write rows without knowing or caring what language the caller uses, the Organization service gives a compiled .NET application compile-time type checking, IntelliSense support for table and column names, and a request/response object model that mirrors the operations it performs. The coordinator service is the kind of application that approach serves.
Connect through the SDK for .NET, not the protocol
The Organization service exposed a direct SOAP endpoint when Dataverse introduced it, but that endpoint is deprecated. It continues to work for now, but Microsoft updates the SDK assemblies so your code keeps working after the endpoint eventually goes away. The practical takeaway: you access the Organization service through the Dataverse SDK for .NET and let the SDK handle the protocol, rather than composing SOAP messages yourself the way you composed OData requests for the Web API.
The SDK centers on the IOrganizationService interface, the contract every connection to the Organization service implements. Two classes provide that contract: CrmServiceClient, the older option, and ServiceClient, found in the Microsoft.PowerPlatform.Dataverse.Client NuGet package.
For new development such as the coordinator service, ServiceClient is the class to reach for, since it supports modern MSAL-based authentication and picks up new capabilities first. The two classes are similar enough from an API perspective that code written against one converts to the other with minimal changes.
Learn more about transitioning apps to the ServiceClient class.
using Microsoft.PowerPlatform.Dataverse.Client;
ServiceClient serviceClient = new ServiceClient(
new Uri("https://contoso.crm.dynamics.com"),
tokenProviderFunction: GetAccessToken,
useUniqueInstance: true);
IOrganizationService service = serviceClient;
Once the coordinator service holds a working IOrganizationService reference, every data operation it performs flows through that single interface.
Think in messages, not method calls
The Web API ties each operation to an HTTP verb: POST to create, PATCH to update. The Organization service takes a different shape. Dataverse defines every operation it supports as a message, each with a name and a set of input and output parameters. You build a strongly typed request object for the message you want, pass it to service.Execute(), and get back a strongly typed response object with the results.
| You want to | Build this request | Get back this response |
|---|---|---|
| Create a row | CreateRequest |
CreateResponse |
| Retrieve one row | RetrieveRequest |
RetrieveResponse |
| Retrieve several rows | RetrieveMultipleRequest |
RetrieveMultipleResponse |
| Update a row | UpdateRequest |
UpdateResponse |
| Delete a row | DeleteRequest |
DeleteResponse |
When the coordinator service runs its scheduled pass, it first needs every contoso_request row still waiting on a decision. It builds a query and executes a RetrieveMultipleRequest to get them all back in one call:
var query = new QueryExpression("contoso_request")
{
ColumnSet = new ColumnSet("contoso_name", "contoso_employeeid"),
Criteria = new FilterExpression
{
Conditions = { new ConditionExpression("contoso_status", ConditionOperator.Equal, 100000000) }
}
};
var retrieveRequest = new RetrieveMultipleRequest { Query = query };
var retrieveResponse = (RetrieveMultipleResponse)service.Execute(retrieveRequest);
EntityCollection pendingRequests = retrieveResponse.EntityCollection;
When a reporting pass calls for aggregation or joins that are awkward to express with QueryExpression or OData, the coordinator service can pass FetchXML to RetrieveMultiple instead. It builds the query as a FetchXML string, wraps it in a FetchExpression, and hands that to the same RetrieveMultiple call, such as tallying pending requests by status:
string fetchXml = @"
<fetch aggregate='true'>
<entity name='contoso_request'>
<attribute name='contoso_status' alias='status' groupby='true' />
<attribute name='contoso_requestid' alias='request_count' aggregate='count' />
<order alias='status' />
</entity>
</fetch>";
EntityCollection results = service.RetrieveMultiple(new FetchExpression(fetchXml));
After the service checks a request against the employee's allowance and the request passes, it moves the row to the next status with an UpdateRequest. Like the Web API's PATCH, the update only needs to carry the column that changed:
var requestToUpdate = new Entity("contoso_request", pendingRequestId);
requestToUpdate["contoso_status"] = new OptionSetValue(100000001); // Approved
var updateRequest = new UpdateRequest { Target = requestToUpdate };
service.Execute(updateRequest);
Notice the shape repeats: build a request, call Execute, read the response. Every operation the coordinator service performs, whether it's creating a row, retrieving one, or updating a batch of them, follows this same pattern. That's part of what makes the Organization service approachable once you've seen it once.
Choose strongly typed or late-bound entities
The examples above use Entity, a generic, late-bound class where you refer to tables and columns by their logical name strings, such as "contoso_request" and "contoso_status". Late binding works without any extra setup, and it adapts to tables and columns added after you compile your code. The trade-off is that a typo in a column name only surfaces when the code runs, not when it compiles.
The SDK also supports early-bound, strongly typed classes generated for your specific tables, such as a generated ContosoRequest class with a ContosoStatus property instead of a string key. Early binding catches a misspelled column name at compile time, gives you IntelliSense while you write the code, and reads more naturally. The cost is that you generate and maintain that class file, and it needs regenerating whenever the table schema changes.
Consider a long-running, compiled service like the coordinator's, where the same four tables stay stable and multiple developers maintain the code over time. For that kind of service, early-bound classes are often worth that maintenance cost.
For code that only touches a table or two, or that needs to work against tables it can't predict in advance, late-bound Entity objects stay the simpler choice. Many real projects mix both: early-bound classes for the tables central to the business logic, late-bound access for anything peripheral.
Learn more about choosing between early-bound and late-bound programming.
Guiding question: Think about the coordinator service's four tables. Which one would you generate an early-bound class for first, and which would you leave late-bound?
Decide when the Organization service is the better fit
The previous unit framed the Web API's strength as reach: anything that can send an HTTP request can use it, which made it the natural choice for the portal and the external inventory service. The Organization service trades that reach for depth. It requires a .NET application and the SDK. In exchange, it gives you compile-time safety, a request/response model matching Dataverse's own operations, and access to messages beyond simple create, retrieve, update, and delete.
That trade-off shows up clearly in one more scenario: plug-ins and custom workflow activities that run inside Dataverse itself. These extensions must use the SDK for .NET. The Web API isn't an option there at all, since the extension runs as compiled code inside the platform's own pipeline.
The coordinator approval service doesn't run inside Dataverse, but it shares the same profile: a trusted, compiled .NET application built around business logic, exactly the context where the Organization service earns its place over the Web API.
Choosing the Organization service doesn't exempt the coordinator service from the limits every Dataverse caller shares. It still issues requests against the same platform, and Dataverse can still throttle those requests under load. The next unit looks at how Dataverse enforces those limits and how to design a retry policy. That policy applies as much to a service.Execute() call as it does to a Web API request.