Querying the Dataverse Web API: FetchXML, OData, and When to Use Each
Dataverse gives you two serious query languages through the Web API: OData query options and FetchXML. Most teams use both, but many use them by habit instead of intent. The result is over-expanded OData calls, unreadable filters, FetchXML where a simple URL would do, and paging bugs that show up only when the customer has real data.
OData is the default for simple Web API reads
Use OData when the shape is straightforward. If you need columns from one table, a few filters, sort order, top count, or a modest related lookup expansion, OData is concise and easy to generate from integration code. It also aligns naturally with the Web API entity set model.
The essential rule is always use $select. Without it, you are asking Dataverse to return far more data than most clients need. That costs network time, serialization time, and sometimes security evaluation time.
GET [Organization URI]/api/data/v9.2/accounts?$select=name,accountnumber,telephone1&$filter=statecode eq 0&$orderby=name asc&$top=25
Accept: application/json
Add $expand carefully. Expanding a primary contact name is fine. Expanding several one-to-many relationships inside a grid API call is usually a performance problem disguised as convenience.
GET [Organization URI]/api/data/v9.2/accounts?$select=name,accountnumber&$expand=primarycontactid($select=fullname,emailaddress1)&$top=10
OData is also easier to compose safely in code when your filters are simple. Encode strings, keep dates explicit, and avoid building query text through casual concatenation.
FetchXML wins when the query is relational or aggregate
Use FetchXML when the query needs CRM-shaped power. FetchXML handles aggregation, group by, distinct, complex link-entity joins, outer joins, and conditions that are awkward or unsupported in plain OData. It is also the language behind many advanced find and view patterns, so Dataverse admins and functional consultants often recognize it quickly.
A common example is counting open cases by customer account. This is not just reading case rows. It is grouping, aggregating, and joining.
<fetch aggregate="true">
<entity name="incident">
<attribute name="incidentid" alias="casecount" aggregate="count" />
<attribute name="customerid" alias="customer" groupby="true" />
<filter>
<condition attribute="statecode" operator="eq" value="0" />
</filter>
<link-entity name="account" from="accountid" to="customerid" link-type="inner" alias="acct">
<attribute name="name" alias="accountname" groupby="true" />
</link-entity>
</entity>
</fetch>
Retrieve FetchXML through the fetchXml query parameter on the entity set. URL encode the XML before sending it. In tooling or scripts, generate the XML as a string, encode it once, and log the readable version for diagnostics.
GET [Organization URI]/api/data/v9.2/incidents?fetchXml=[URL encoded FetchXML]
Accept: application/json
FetchXML is not automatically faster. It is more expressive. You still need selective filters, sensible columns, and paging.
The choice is about query shape, not team preference
Pick the language that makes the intent obvious. If the query is a list of active accounts with three columns, OData is clearer. If it is distinct contacts from accounts that have open premium cases and no activity in 30 days, FetchXML will probably be easier to test and maintain.
| Need | Prefer | Reason |
|---|---|---|
| Simple table read | OData | Compact URL and easy client composition |
| Column projection | OData | $select is direct and readable |
| Basic lookup expansion | OData | $expand works well for small related shapes |
| Aggregation | FetchXML | Native aggregate and group by support |
| Complex joins | FetchXML | link-entity is built for Dataverse relationships |
| Distinct result sets | FetchXML | Explicit distinct behavior |
| Saved view parity | FetchXML | Easier to align with model-driven query definitions |
The wrong choice is the one that hides complexity. A 600-character OData $filter with nested relationship assumptions is not maintainable. A FetchXML block for a two-column lookup list is noise.
Paging must be handled as part of the contract
Never assume the first page is the data set. OData responses can include @odata.nextLink. If it exists, call it exactly as returned until it disappears. Do not rebuild the next URL by hand unless you enjoy subtle paging bugs.
let url = `${orgUrl}/api/data/v9.2/accounts?$select=name,accountnumber&$top=5000`;
while (url) {
const response = await fetch(url, { headers });
const page = await response.json();
for (const row of page.value) {
processAccount(row);
}
url = page["@odata.nextLink"] ?? "";
}
FetchXML paging commonly uses page number, count, and paging cookie patterns. When using SDKs or helper libraries, let them manage the cookie if possible. If you call the Web API directly, preserve the paging cookie exactly and encode it correctly for the next request.
Paging bugs usually stay hidden in development because test environments have tiny data. Build paging into the first version of the integration, not after the first production export truncates records.
Performance starts with smaller answers
The fastest query is the one that asks for less. Always project only the columns needed. Apply filters early. Avoid wide $expand calls that pull entire related records. Prefer separate targeted requests over one monstrous query when the joined shape explodes row counts.
For OData, be strict about $select inside $expand too. Expanding a contact without selecting columns can return far more than the UI displays. For FetchXML, include only the attributes required by the caller. Do not copy a view definition with twenty columns into an integration that needs two.
Also respect Dataverse service protection limits. Parallel requests can improve throughput, but uncontrolled fan-out can throttle the whole job. Batch carefully, add retry with backoff, and log correlation ids for failures.
Debuggability matters in production integrations
Log the logical query, not just the final URL. URLs with encoded FetchXML are hard to read during an incident. Store the unencoded FetchXML in debug logs when safe, and store the final request URL separately. For OData, log the entity set, selected columns, filters, and page progress.
When a query supports a customer-facing process, add tests with realistic data volume and security roles. Dataverse security trimming can change results. A system administrator test does not prove what an integration user or portal user can retrieve.
Use OData for simple, resource-oriented reads. Use FetchXML when the query is relational, aggregate, distinct, or view-like. Both are first-class paths through the Dataverse Web API. Senior engineering is not picking a favorite; it is choosing the query shape that stays correct, fast, and readable when the data gets real.
Keep reading
Dual-Write vs Virtual Entities vs OData: Choosing the Right F&O–Dataverse Pattern
Compare dual-write, virtual entities, OData, and DMF for Finance and Operations to Dataverse integration with latency and failure tradeoffs.
Low-Code Plug-ins in Dataverse: Server-Side Logic Without C#
Learn when to use Dataverse low-code plug-ins with Power Fx for transactional server-side logic, and when C# plug-ins or flows still win.
Writing Your First Dataverse Plug-in in C#: A Complete Walkthrough
Build a first Dataverse C# plug-in with IPlugin, pipeline stages, Target handling, tracing, registration, and production-safe practices.
Newsletter
New posts, straight to your inbox
One email per post. No spam, no tracking pixels, unsubscribe anytime.
Comments
No comments yet. Be the first.