You can specify a limit on the number of rows retrieved for each request by setting a page size. Using paging, you can retrieve consecutive pages of data representing all the records that match the criteria of a query in a performant manner.
The default and maximum page size is 5,000 rows. If you don't set a page size, Dataverse will return up to 5,000 rows of data at a time. To get more rows, you must send additional requests.
Note
Don't use the fetch elementtop attribute with paging. These different methods of limiting the results of a query are not compatible.
With simple paging, sometimes called legacy paging, Dataverse retrieves all the results of the query up to the current page, selects the number of records needed for the page and then ignores the rest. This allows for quickly paging backward and forward though the data or skipping to a specific page. However the total number of records is limited to 50,000 and there can be performance issues for complex queries and arbitrarily sorted distinct query results.
Simple paging works well for small data sets, but as the number of rows in the data set increases, performance suffers. The total number of rows that can be retrieved using simple paging is 50,000. For best performance in all cases, we recommend consistently using paging cookies.
Paging cookies
When there are more rows to retrieve after requesting the first page, Dataverse usually returns a paging cookie to be used on the following requests for the next pages.
The paging cookie contains data about the first and last record in the results and helps Dataverse retrieve the next row of data as quickly as possible and should be used when provided. You shouldn't modify the data in the paging cookie, just set the value to the fetch elementpaging-cookie attribute and increment the page attribute value for subsequent requests.
Queries that don't support paging cookies
Some queries do not support paging cookies. When paging cookies aren't supported by a query, no paging cookie value is returned with the result. For example, queries sorted using a link-entity attribute may not support paging cookies.
When Dataverse doesn't return a paging cookie, the paging model falls back to simple paging, with all the limitations that come with it.
Paging cookie examples
How you use paging cookies depends on whether you are using the SDK for .NET or Web API.
The following RetrieveAll static method will return all records that match the FetchXml query, sending multiple requests if the number of records exceeds the page size.
/// <summary>
/// Returns all records matching the criteria
/// </summary>
/// <param name="service">The authenticated IOrganizationService instance.</param>
/// <param name="fetchXml">The fetchXml Query string</param>
/// <param name="pageSize">The page size to use. Default is 5000</param>
/// <returns>All the records that match the criteria</returns>
static EntityCollection RetrieveAll(IOrganizationService service, string fetchXml, int pageSize = 5000)
{
// The records to return
List<Entity> entities = new();
XElement fetchNode = XElement.Parse(fetchXml);
int page = 1; //Start with page 1
//Set the page
fetchNode.SetAttributeValue("page", page);
// Set the page size
fetchNode.SetAttributeValue("count", pageSize);
while (true)
{
// Get the page
EntityCollection results = service.RetrieveMultiple(new FetchExpression(fetchNode.ToString()));
entities.AddRange(results.Entities);
if (!results.MoreRecords)
{
break;
}
// Set the fetch paging-cookie attribute with the paging cookie from the previous query
fetchNode.SetAttributeValue("paging-cookie", results.PagingCookie);
fetchNode.SetAttributeValue("page", ++page);
}
return new EntityCollection(entities);
}
Add the RetrieveAll static method to the Program class.
Modify the Main method as shown below:
static void Main(string[] args)
{
using (ServiceClient serviceClient = new(connectionString))
{
if (serviceClient.IsReady)
{
//WhoAmIResponse response =
// (WhoAmIResponse)serviceClient.Execute(new WhoAmIRequest());
//Console.WriteLine("User ID is {0}.", response.UserId);
string fetchQuery = @"<fetch count='3' page='1'>
<entity name='contact'>
<attribute name='fullname'/>
<attribute name='jobtitle'/>
<attribute name='annualincome'/>
<order descending='true' attribute='fullname'/>
</entity>
</fetch>";
EntityCollection records = RetrieveAll(service: serviceClient,
fetchXml: fetchQuery,
pageSize: 25);
Console.WriteLine($"Success: {records.Entities.Count}");
}
else
{
Console.WriteLine(
"A web service connection was not established.");
}
}
// Pause the console so it does not close.
Console.WriteLine("Press the <Enter> key to exit.");
Console.ReadLine();
}
Important
This query will return ALL records that match the criteria. Make sure you include filter elements to limit the results.
With the Web API you must request a paging cookie as an annotation. Use either of these request headers:
Prefer: odata.include-annotations="Microsoft.Dynamics.CRM.fetchxmlpagingcookie,Microsoft.Dynamics.CRM.morerecords"
OR for all annotations:
Prefer: odata.include-annotations="*"
And these annotations will be returned with the result:
@Microsoft.Dynamics.CRM.fetchxmlpagingcookie
@Microsoft.Dynamics.CRM.morerecords
The following series of FetchXML requests show the use of paging cookies. This example uses a small count value (3) for brevity.
In the response, the @Microsoft.Dynamics.CRM.morerecords annotation value indicates that more records exist that match the criteria.
The @Microsoft.Dynamics.CRM.fetchxmlpagingcookie annotation value provides the paging information about the record returned. The @Microsoft.Dynamics.CRM.fetchxmlpagingcookie value is an XML element. You need to use the pagingcookie attribute value of that element in the next request.
The pagingcookie attribute value is URL-encoded twice. Although you don't need to check this, the decoded and formatted value in this example looks like this:
In all subsequent requests where the previous page @Microsoft.Dynamics.CRM.morerecords annotation value is true, you need to:
Increment the fetch element page attribute value.
URL-decode the pagingcookie attribute value twice.
XML-encode the decoded pagingcookie attribute value and set it as the value of a paging-cookie attribute on the fetch element.
Note
Whether you must explicitly XML-encode the value may depend on the technology you use. In .NET, it might be done for you when you set the XML value to an attribute of another XML element.
URL Encode the entire FetchXml value as you did in the first request.
In the following request, the FetchXML looks like this before it's URL-encoded:
On the final page, the @Microsoft.Dynamics.CRM.morerecords and @Microsoft.Dynamics.CRM.fetchxmlpagingcookie annotations aren't included in the response.
When using C# with HttpClient, the following RetrieveAll static method will return all records that match the FetchXml query, sending multiple requests if the number of records exceeds the page size.
/// <summary>
/// Returns all records matching the criteria
/// </summary>
/// <param name="client">The authenticated HttpClient instance.</param>
/// <param name="entitySetName">The EntitySetName for the table used in the fetchXml</param>
/// <param name="fetchXml">The FetchXml query string</param>
/// <param name="pageSize">The page size to use. Default is 5000</param>
/// <returns>All the records that match the criteria</returns>
/// <exception cref="Exception"></exception>
static async Task<List<JsonObject>> RetrieveAll(HttpClient client,
string entitySetName,
string fetchXml,
int pageSize = 5000)
{
List<JsonObject> entities = new();
XElement fetchNode = XElement.Parse(fetchXml);
int page = 1; //Start with page 1
// Set the fetch page attribute
fetchNode.SetAttributeValue("page", page);
// Set the fetch count attribute
fetchNode.SetAttributeValue("count", pageSize);
while (true)
{
bool moreRecords;
string pagingCookie = null;
// Prepare the request
HttpRequestMessage request = new()
{
Method = HttpMethod.Get,
RequestUri = new Uri(
uriString: $"{entitySetName}?fetchXml={HttpUtility.UrlEncode(fetchNode.ToString())}",
uriKind: UriKind.Relative),
};
// Add annotations to return formatted values
request.Headers.Add("Prefer", "odata.include-annotations=" +
"\"Microsoft.Dynamics.CRM.fetchxmlpagingcookie," +
"Microsoft.Dynamics.CRM.morerecords\"");
// Send the request
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
string jsonContent = await response.Content.ReadAsStringAsync();
// Using System.Text.Json.Nodes
JsonObject content = JsonNode.Parse(jsonContent)?.AsObject();
// Records are in the 'value' property
content.TryGetPropertyValue("value", out JsonNode records);
// Add records to the list
entities.AddRange(records.AsArray().Cast<JsonObject>());
// Detect if there are more records
moreRecords = content.
TryGetPropertyValue("@Microsoft.Dynamics.CRM.morerecords", out _);
if (moreRecords)
{
// Get the paging cookie value
if (content.
TryGetPropertyValue("@Microsoft.Dynamics.CRM.fetchxmlpagingcookie",
out JsonNode fetchxmlpagingcookie))
{
pagingCookie = fetchxmlpagingcookie.AsValue().ToString();
}
}
}
else
{
throw new Exception($"Web API call failed. Status Code: {response.StatusCode}");
}
if (!moreRecords)
{
// Stop sending requests
break;
}
XElement cookieElement = XElement.Parse(pagingCookie);
// Extract the pagingcookie attribute
XAttribute pagingcookieAttribute = cookieElement.Attribute("pagingcookie");
// Decode the pagingcookie attribute twice
pagingCookie = HttpUtility.UrlDecode(HttpUtility.UrlDecode(pagingcookieAttribute.Value));
// Set the fetch paging-cookie attribute with the paging cookie from the previous query
fetchNode.SetAttributeValue("paging-cookie", pagingCookie);
// Increment the fetch page attribute value
fetchNode.SetAttributeValue("page", ++page);
}
// Return the records from all requests
return entities;
}
Add the RetrieveAll static method to the Program class.
Modify the Main method and replace the content of the Web API call region as shown below:
#region Web API call
string fetchXml = @"<fetch>
<entity name='contact'>
<attribute name='fullname'/>
<attribute name='jobtitle'/>
<attribute name='annualincome'/>
<order descending='true' attribute='fullname'/>
</entity>
</fetch>";
List<JsonObject> records = await RetrieveAll(client: client,
entitySetName: "contacts",
fetchXml: fetchXml,
pageSize: 3);
Console.WriteLine($"Success: {records.Count}");
#endregion Web API call
Important
This query will return ALL records that match the criteria. Make sure you include filter elements to limit the results.
The entitySetName parameter must be the entity set name for the same table specified in the FetchXml entity elementname attribute.
Ordering and paging
How a page is ordered makes a big difference when paging data. If the information about how the results are ordered is ambiguous, Dataverse can't consistently or efficiently return paged data.
Specify an order for your query. With FetchXml, if you don't add any order elements to your query, Dataverse adds an order based on the primary key of the table. However QueryExpression does not, and when your query specifies distinct results, no primary key values are returned, so Dataverse can't add this default order. You must specify a paging order. Without any order specified, distinct query results might be returned in random order. OData doesn't provide any option to return distinct results, but you should still apply an order when retrieving paged results.
Paging is dynamic. Each request is evaluated independently as they're received. A paging cookie tells Dataverse the previous page. With this paging cookie data, Dataverse can start with the next record after the last one on the preceding page.
Paging works best going forward. If you go back and retrieve a page you previously retrieved, the results can be different because records could be added, deleted, or modified during since you last retrieved the page. In other words, if your page size is 50 and you go back, you get 50 records, but they might not be the same 50 records. If you keep progressing forward through the pages of a data set, you can expect all the records are returned in a consistent sequence.
Deterministic ordering is important
Deterministic ordering means that there's a way to calculate an order consistently. With a given set of records, the records are always returned in the same order. If you need consistent orders and paging, you must include some unique values or combination of column values that are and specify an order for them to be evaluated.
Nondeterministic example
Let's look at an example that is nondeterministic. This data set contains only State and Status information and is filtered to only return records in an open State. The results are ordered by Status. The first three pages are requested. The results look like this:
State
Status
Page
Open
Active
1 Start
Open
Active
1
Open
Active
1 End
Open
Active
Open
Active
Open
Inactive
Open
Inactive
The paging cookie saves information about the last record on the page. When the next page is requested, the last record from the first page isn't included. However, given the nondeterministic data, there's no guarantee that the other two records on the first page aren't included in the second page.
To achieve deterministic ordering, add orders on columns that contain unique values, or values that are semi-unique.
Deterministic example
This query is like the nondeterministic one, but it includes the Case ID column that includes unique values. It's also ordered by Status, but also ordered using Case ID. The results look like this:
State
Status
Case ID
Page
Open
Active
Case-0010
1 Start
Open
Active
Case-0021
1
Open
Active
Case-0032
1 End
Open
Active
Case-0034
Open
Active
Case-0070
Open
Inactive
Case-0015
Open
Inactive
Case-0047
In the next page, the cookie will have Case-0032 stored as the last record in the first page, so page two will start with the next record after that record. The results look like this:
State
Status
Case ID
Page
Open
Active
Case-0010
1 Start
Open
Active
Case-0021
1
Open
Active
Case-0032
1 End
Open
Active
Case-0034
2 Start
Open
Active
Case-0070
2
Open
Inactive
Case-0015
2 End
Open
Inactive
Case-0047
Because this query orders unique column values, the order is consistent.
Best practices for orders when paging data
Note
When possible, queries should order on the primary key for the table because Dataverse is optimized for ordering on the primary key by default. Ordering by non-unique or complex fields cause excess overhead and slower queries.
When you retrieve a limited set of data to display in an application, or if you need to return more than 5,000 rows of data, you need to page the results. The choices you make in determining the order of the results can determine whether the rows in each page of data you retrieve overlaps with other pages. Without proper ordering, the same record can appear in more than one page.
To prevent the same record from appearing in more than one page, apply the following best practices:
It's best to include a column that has a unique identifier. For example:
Table primary key columns
Autonumber columns
User/contact IDs
If you can't include a column with a unique identifier, include multiple fields that will most likely result in unique combinations. For example:
First name + last name + email address
Full name + email address
Email address + company name
Anti-patterns for orders when paging data
The following are ordering choices to avoid:
Orders that don't include unique identifiers
Orders on calculated fields
Orders that have single or multiple fields that aren't likely to provide uniqueness such as:
Status and state
Choices or Yes/No
Name values by themselves. For example name, firstname, lastname
Text fields like titles, descriptions, and multi-line text