Dynamics 365 Integration: REST vs OData vs SDK
Integrating with Dynamics 365 means choosing between REST, OData, and the SDK. Each has different performance characteristics, authentication models, and trade-offs. After years of building integrations with Dynamics 365, here's my field guide.
The Three Approaches
| REST/Web API | OData | SDK (IOrganizationService) | |
|---|---|---|---|
| **Protocol** | HTTP + JSON | HTTP + JSON (OData conventions) | .NET SDK (HTTP under the hood) |
| **Auth** | OAuth 2.0 / Azure AD | OAuth 2.0 / Azure AD | Connection string or OAuth |
| **Batching** | `$batch` endpoint | `$batch` endpoint | `ExecuteMultipleRequest` |
| **Change tracking** | Manual | `odata.track-changes` | `RetrieveEntityChanges` |
| **Best for** | Non-.NET clients | Complex queries, filtering | .NET plugins, workflows |
REST/Web API: Maximum Flexibility
The Web API works from any language and platform. It's the right choice for non-.NET integrations or when you need full control over HTTP requests:
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync(
$"{crmUrl}/api/data/v9.2/contacts?" +
"$select=fullname,emailaddress1" +
"&$filter=statecode eq 0" +
"&$top=100");
var result = await response.Content
.ReadFromJsonAsync<ODataResponse<Contact>>();**Performance tip:** Always use `$select` to limit returned fields. A full contact record can be 5KB+ of JSON. With `$select`, it's a few hundred bytes.
OData: Query Power
OData adds powerful query capabilities on top of REST. Complex filters, aggregations, and joins are possible without multiple roundtrips:
GET /api/data/v9.2/accounts?
$select=name,revenue
&$expand=contact_customer_accounts(
$select=fullname,emailaddress1;
$filter=statecode eq 0
)
&$filter=revenue gt 1000000
&$orderby=revenue desc
&$top=50This single request retrieves high-value accounts with their active contacts — something that would require multiple queries with the SDK.
FetchXML via Web API
For even more complex queries, you can pass FetchXML through the Web API:
var fetchXml = @"
<fetch top='50' aggregate='true'>
<entity name='opportunity'>
<attribute name='estimatedvalue' alias='total'
aggregate='sum' />
<attribute name='ownerid' alias='owner'
groupby='true' />
<filter>
<condition attribute='statecode' operator='eq'
value='0' />
</filter>
</entity>
</fetch>";
var encoded = Uri.EscapeDataString(fetchXml);
var response = await client.GetAsync(
$"{crmUrl}/api/data/v9.2/opportunities?fetchXml={encoded}");SDK: The .NET Native Choice
Inside Dynamics 365 plugins and custom workflow activities, the SDK is the only option. Outside of Dynamics, it's still the most ergonomic choice for .NET:
var service = new ServiceClient(connectionString);
var query = new QueryExpression("contact")
{
ColumnSet = new ColumnSet("fullname", "emailaddress1"),
Criteria =
{
Conditions =
{
new ConditionExpression(
"statecode", ConditionOperator.Equal, 0)
}
},
TopCount = 100
};
var contacts = service.RetrieveMultiple(query);Batch Operations With ExecuteMultiple
For bulk operations, `ExecuteMultipleRequest` is essential. Without it, creating 1000 records means 1000 HTTP roundtrips:
var batch = new ExecuteMultipleRequest
{
Requests = new OrganizationRequestCollection(),
Settings = new ExecuteMultipleSettings
{
ContinueOnError = true,
ReturnResponses = false
}
};
foreach (var contact in contacts)
{
batch.Requests.Add(new CreateRequest { Target = contact });
if (batch.Requests.Count >= 200)
{
service.Execute(batch);
batch.Requests.Clear();
}
}
if (batch.Requests.Count > 0)
service.Execute(batch);**Important:** Keep batch sizes at 200 or fewer. Larger batches risk timeouts and are harder to retry on partial failure.
My Decision Framework
- **Building a plugin or workflow?** → SDK, no choice
- **Non-.NET client (Python, Node)?** → REST/Web API
- **Complex queries with joins/aggregations?** → OData or FetchXML via Web API
- **Bulk data operations in .NET?** → SDK with ExecuteMultiple
- **Simple CRUD from .NET?** → SDK for ergonomics, REST for portability
Performance Comparison
From benchmarks on a real Dynamics 365 Online instance:
| Operation | REST | OData | SDK |
|---|---|---|---|
| Single read | 120ms | 125ms | 130ms |
| Filtered list (100) | 180ms | 175ms | 190ms |
| Create single | 250ms | 250ms | 260ms |
| Batch create (200) | 3.2s | 3.1s | 2.8s |
The differences are marginal for single operations. The SDK has a slight edge in batch operations due to optimized serialization.
Key Takeaways
- All three approaches use HTTP under the hood — performance differences are minimal
- Use `$select` always, batch always — these matter more than which API you choose
- OData's query capabilities can eliminate multiple roundtrips
- The SDK is mandatory for plugins and the best ergonomic choice for .NET
- Pick based on your platform, query complexity, and team familiarity
Share this article
About the Author
Georg is a senior solution architect specializing in .NET, Azure, and Dynamics 365. He helps organizations design and build scalable, maintainable enterprise systems. When he's not writing code, he's writing about it here.
Learn more about Georg