Learn how to work with collections, handle pagination, and navigate large result sets in Microsoft Graph
Microsoft Graph API returns collections of items for many requests. The SDK provides strongly-typed collection responses with built-in pagination support to efficiently handle large result sets.
Whenever a request returns multiple items (users, messages, files, etc.), the Microsoft Graph API returns a collection. The SDK generates collection response objects that contain:
Value - A List<T> of items in the current page
OdataNextLink - A URL to get the next page (if available)
OdataCount - Total count of items (when $count=true is used)
AdditionalData - Dictionary for any additional properties
From collections.md:18-24:
Property
Description
Value
A List<Item>
NextLink
A string representing the URL used to get to the next page of items, if another page exists. This value will be null if there is not a next page.
AdditionalData
An IDictionary<string, object> for any additional values returned by the service.
using Microsoft.Graph;using Microsoft.Graph.Models;// Get all messages (first page)var messages = await graphClient.Me.Messages.GetAsync();// Access the collection itemsforeach (var message in messages.Value){ Console.WriteLine($"{message.Subject} - {message.ReceivedDateTime}");}Console.WriteLine($"Retrieved {messages.Value.Count} messages");
From collections.md:10-14:
await graphClient .Groups .GetAsync();
The GetAsync() method returns an IParsable implementation that typically includes the Value property containing the list of items.
When a collection has more items than can be returned in a single response, the API uses pagination. The SDK provides the OdataNextLink property to navigate through pages.
public class PageIterator<T>{ public static async Task<List<T>> GetAllPagesAsync<TResponse>( TResponse firstPage, Func<string, Task<TResponse>> getNextPage, Func<TResponse, List<T>> getValue, Func<TResponse, string> getNextLink, int maxPages = int.MaxValue) { var allItems = new List<T>(); var currentPage = firstPage; int pageCount = 0; while (currentPage != null && pageCount < maxPages) { var items = getValue(currentPage); if (items != null) { allItems.AddRange(items); } var nextLink = getNextLink(currentPage); if (string.IsNullOrEmpty(nextLink)) { break; } currentPage = await getNextPage(nextLink); pageCount++; } return allItems; }}// Usagevar firstPage = await graphClient.Users.GetAsync(config => config.QueryParameters.Top = 100);var allUsers = await PageIterator<User>.GetAllPagesAsync( firstPage, nextLink => new Microsoft.Graph.Users.UsersRequestBuilder(nextLink, graphClient.RequestAdapter).GetAsync(), page => page.Value, page => page.OdataNextLink);Console.WriteLine($"Retrieved {allUsers.Count} users total");
Be cautious when retrieving all pages from large collections. This can consume significant memory and API quota. Consider processing pages incrementally or using filtering to reduce result sets.
Delta queries allow you to track changes in a collection over time.
// Get initial deltavar deltaResponse = await graphClient.Users.Delta.GetAsync(config =>{ config.QueryParameters.Select = new[] { "displayName", "mail" };});Console.WriteLine($"Initial sync: {deltaResponse.Value.Count} users");// Store the delta link for future queriesstring deltaLink = deltaResponse.OdataDeltaLink;// Later, get only changes since last queryif (!string.IsNullOrEmpty(deltaLink)){ var changesBuilder = new Microsoft.Graph.Users.Delta.DeltaRequestBuilder( deltaLink, graphClient.RequestAdapter ); var changes = await changesBuilder.GetAsync(); Console.WriteLine($"Changes since last sync: {changes.Value.Count} users"); foreach (var user in changes.Value) { Console.WriteLine($"Changed: {user.DisplayName}"); } // Update delta link deltaLink = changes.OdataDeltaLink;}
// Good - small payloadvar users = await graphClient.Users.GetAsync(config =>{ config.QueryParameters.Select = new[] { "id", "displayName" }; config.QueryParameters.Top = 100;});// Bad - large payload with unnecessary datavar users = await graphClient.Users.GetAsync(config => config.QueryParameters.Top = 100);
Use appropriate page sizes
Balance between number of requests and response size:
// Too small - many requestsconfig.QueryParameters.Top = 10;// Too large - slow responses, memory issuesconfig.QueryParameters.Top = 999;// Good - balancedconfig.QueryParameters.Top = 100; // or 50-200 depending on item size
// Good - process page by pagevar users = await graphClient.Users.GetAsync(config => config.QueryParameters.Top = 100);do{ await ProcessUsersAsync(users.Value); // Process current page if (!string.IsNullOrEmpty(users.OdataNextLink)) { var nextBuilder = new Microsoft.Graph.Users.UsersRequestBuilder( users.OdataNextLink, graphClient.RequestAdapter ); users = await nextBuilder.GetAsync(); } else { break; }} while (true);
Use delta queries for sync scenarios
When tracking changes over time, use delta queries:
// Initial syncvar delta = await graphClient.Users.Delta.GetAsync();await ProcessUsersAsync(delta.Value);string deltaLink = delta.OdataDeltaLink;// Later - only get changesvar changes = await new Microsoft.Graph.Users.Delta.DeltaRequestBuilder( deltaLink, graphClient.RequestAdapter).GetAsync();await ProcessChangesAsync(changes.Value);
// Ensure you're using the full next link URLvar firstPage = await graphClient.Users.GetAsync();if (!string.IsNullOrEmpty(firstPage.OdataNextLink)){ // Correct - use the full URL var nextBuilder = new Microsoft.Graph.Users.UsersRequestBuilder( firstPage.OdataNextLink, graphClient.RequestAdapter ); var nextPage = await nextBuilder.GetAsync();}