-
Notifications
You must be signed in to change notification settings - Fork 270
Pagination for microsoft graph module #3940
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pagination for microsoft graph module #3940
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the design so far.
Following ideas for test cases:
- Request two items (no NextLink) -> Verify that HasMorePages returns false + NextLink is empty
- Request two items (with NextLink) -> Verify that HasMorePages returns true + NextLink contains a value.
src/System Application/App/MicrosoftGraph/src/GraphPaginationData.Codeunit.al
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No objections to the code in its current shape. It's coming along nicely!
Add tests for pagination methods and codeunits
|
I’ve completed development and testing, including validation on a real tenant. The code behaves as expected. Additionally, I’ve prepared test codeunits for the new pagination methods and codeunits. I’ve also written a README with usage instructions for these procedures (see below). |
Microsoft Graph API Pagination GuideThis guide explains how to use the new pagination functionality in the Microsoft Graph module for Business Central. Pagination is essential when working with large datasets from Microsoft Graph API. Table of Contents
OverviewMicrosoft Graph API returns data in pages to improve performance. When querying large datasets (e.g., all users in a tenant), the API returns a subset of results along with a The new pagination functionality provides three main approaches:
Key ComponentsGraphPaginationData CodeunitManages pagination state including:
New Graph Client Methods
Usage ExamplesPrerequisitesAll examples assume you have initialized the Graph Client with proper authentication: var
GraphClient: Codeunit "Graph Client";
GraphAuthorization: Codeunit "Graph Authorization";
GraphAuthInterface: Interface "Graph Authorization";
begin
// Initialize with your authentication method
GraphAuthInterface := GraphAuthorization.CreateAuthorizationWithClientCredentials(
TenantId,
ClientId,
ClientSecret,
'https://graph.microsoft.com/.default'
);
GraphClient.Initialize(Enum::"Graph API Version"::"v1.0", GraphAuthInterface);
end;1. Simple Automatic PaginationThe easiest way to get all results - let the module handle pagination automatically: procedure GetAllUsersSimple()
var
GraphClient: Codeunit "Graph Client";
GraphOptionalParameters: Codeunit "Graph Optional Parameters";
HttpResponseMessage: Codeunit "Http Response Message";
AllUsers: JsonArray;
begin
// Get all users automatically - the module handles pagination internally
if GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, AllUsers) then
Message('Retrieved %1 users', AllUsers.Count())
else
Error('Failed to retrieve users: %1', HttpResponseMessage.GetReasonPhrase());
end;2. Manual Page-by-Page ProcessingFor better control and memory management with large datasets: procedure ProcessUsersPageByPage()
var
GraphClient: Codeunit "Graph Client";
GraphPaginationData: Codeunit "Graph Pagination Data";
GraphOptionalParameters: Codeunit "Graph Optional Parameters";
HttpResponseMessage: Codeunit "Http Response Message";
PageNumber: Integer;
TotalUsers: Integer;
begin
// Configure pagination
GraphPaginationData.SetPageSize(25); // 25 items per page
// Optional: Select specific fields to reduce payload
GraphOptionalParameters.SetODataQueryParameter(
Enum::"Graph OData Query Parameter"::select,
'id,displayName,mail'
);
// Get first page
if not GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage) then
Error('Failed to retrieve first page');
PageNumber := 1;
ProcessPage(HttpResponseMessage, PageNumber, TotalUsers);
// Process remaining pages
while GraphPaginationData.HasMorePages() do begin
if not GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage) then
Error('Failed to retrieve page %1', PageNumber + 1);
PageNumber += 1;
ProcessPage(HttpResponseMessage, PageNumber, TotalUsers);
end;
Message('Processed %1 users across %2 pages', TotalUsers, PageNumber);
end;
local procedure ProcessPage(HttpResponseMessage: Codeunit "Http Response Message"; PageNumber: Integer; var TotalUsers: Integer)
var
ResponseJson: JsonObject;
ValueArray: JsonArray;
JsonToken: JsonToken;
begin
if ResponseJson.ReadFrom(HttpResponseMessage.GetContent().AsText()) then
if ResponseJson.Get('value', JsonToken) then begin
ValueArray := JsonToken.AsArray();
TotalUsers += ValueArray.Count();
// Process each item in the current page
// Your business logic here...
end;
end;3. Filtered PaginationCombine pagination with OData filters for efficient data retrieval: procedure GetFilteredUsers()
var
GraphClient: Codeunit "Graph Client";
GraphOptionalParameters: Codeunit "Graph Optional Parameters";
HttpResponseMessage: Codeunit "Http Response Message";
FilteredUsers: JsonArray;
begin
// Filter for enabled users whose display name starts with 'A'
GraphOptionalParameters.SetODataQueryParameter(
Enum::"Graph OData Query Parameter"::filter,
'accountEnabled eq true and startswith(displayName, ''A'')'
);
// Select specific fields
GraphOptionalParameters.SetODataQueryParameter(
Enum::"Graph OData Query Parameter"::select,
'id,displayName,mail,accountEnabled,createdDateTime'
);
// For complex queries, might need consistency level
GraphOptionalParameters.SetConsistencyLevelRequestHeader('eventual');
// Get all filtered results
if GraphClient.GetAllPages('users', GraphOptionalParameters, HttpResponseMessage, FilteredUsers) then
Message('Found %1 filtered users', FilteredUsers.Count())
else
Error('Failed to retrieve filtered users');
end;4. Getting Total CountGet the total count before processing to show progress or confirm with user: procedure GetUsersWithCount()
var
GraphClient: Codeunit "Graph Client";
GraphPaginationData: Codeunit "Graph Pagination Data";
GraphOptionalParameters: Codeunit "Graph Optional Parameters";
HttpResponseMessage: Codeunit "Http Response Message";
ResponseJson: JsonObject;
JsonToken: JsonToken;
TotalCount: Integer;
begin
// Request count
GraphOptionalParameters.SetODataQueryParameter(
Enum::"Graph OData Query Parameter"::count,
'true'
);
GraphOptionalParameters.SetConsistencyLevelRequestHeader('eventual');
// Set page size
GraphPaginationData.SetPageSize(50);
// Get first page with count
if GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage) then begin
if ResponseJson.ReadFrom(HttpResponseMessage.GetContent().AsText()) then
if ResponseJson.Get('@odata.count', JsonToken) then begin
TotalCount := JsonToken.AsValue().AsInteger();
Message('Total users in tenant: %1', TotalCount);
// Decide whether to continue based on count
if TotalCount > 1000 then
if not Confirm('There are %1 users. Continue?', false, TotalCount) then
exit;
// Process remaining pages...
end;
end;
end;Advanced UsageCombining with Skip ParameterSkip a certain number of records before starting pagination: // Skip first 100 users, then paginate
GraphOptionalParameters.SetODataQueryParameter(
Enum::"Graph OData Query Parameter"::skip,
'100'
);
GraphPaginationData.SetPageSize(25);
GraphClient.GetWithPagination('users', GraphOptionalParameters, GraphPaginationData, HttpResponseMessage);Progress Dialog for Large DatasetsShow progress when processing many pages: procedure ProcessWithProgress(var GraphClient: Codeunit "Graph Client"; var GraphPaginationData: Codeunit "Graph Pagination Data"; TotalCount: Integer)
var
ProgressDialog: Dialog;
HttpResponseMessage: Codeunit "Http Response Message";
ProcessedCount: Integer;
begin
ProgressDialog.Open('Processing users\@1@@@@@@@@@@@@@@@@@@@@', ProcessedCount);
while GraphPaginationData.HasMorePages() do
if GraphClient.GetNextPage(GraphPaginationData, HttpResponseMessage) then begin
ProcessedCount += GetPageItemCount(HttpResponseMessage);
ProgressDialog.Update(1, Round(ProcessedCount / TotalCount * 10000, 1));
end;
ProgressDialog.Close();
end;Best Practices
API ReferenceGraph Client MethodsGetWithPaginationprocedure GetWithPagination(
RelativeUriToResource: Text;
GraphOptionalParameters: Codeunit "Graph Optional Parameters";
var GraphPaginationData: Codeunit "Graph Pagination Data";
var HttpResponseMessage: Codeunit "Http Response Message"
): BooleanFetches the first page of results with pagination support. GetNextPageprocedure GetNextPage(
var GraphPaginationData: Codeunit "Graph Pagination Data";
var HttpResponseMessage: Codeunit "Http Response Message"
): BooleanRetrieves the next page using the stored pagination data. GetAllPagesprocedure GetAllPages(
RelativeUriToResource: Text;
GraphOptionalParameters: Codeunit "Graph Optional Parameters";
var HttpResponseMessage: Codeunit "Http Response Message";
var JsonResults: JsonArray
): BooleanAutomatically fetches all pages and combines results into a single JsonArray. GraphPaginationData Methods
See Also |
src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al
Outdated
Show resolved
Hide resolved
src/System Application/Test/MicrosoftGraph/src/GraphPaginationIntegTest.Codeunit.al
Show resolved
Hide resolved
src/System Application/Test/MicrosoftGraph/src/GraphPaginationDataTest.Codeunit.al
Show resolved
Hide resolved
|
Looks like we're getting there! Just in time for 2025 wave 2 🥳 I'll run CI, give this a thorough review, do the ADO bookkeeping and in it goes! |
|
Great, I would like to use this part of the code in Sharepoint Graph Module, so I waited until it appeared in master for #3655 to finish that as well. |
|
Closing and reopening PR for force a rebuild. |
|
I see that I forgot to put "InternaslVisibleTo" for Graph Pagination Helper, will fix it soon. |
|
I want to merge this PR this week, to get this in in time for 2025 wave 2. I'm currently having some issues with the bookkeeping. It won't create the right feature / slice for me in ADO for the linked Idea. This has nothing to do with the code, but is what is holding me back right now. I'll get that sorted out (probably create all work items manually) and then we'll get this merged! Stay tuned 🙂 |
|
I will keep closely monitoring the PR and respond if necessary, please let me know if you need any assistance from me. As I understand it, there’s no chance we will finish #3655 in time for the 2025 Release Wave 2? That PR is essentially waiting on this PR. |
src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al
Outdated
Show resolved
Hide resolved
src/System Application/App/MicrosoftGraph/src/GraphClient.Codeunit.al
Outdated
Show resolved
Hide resolved
Maybe not 2025 wave 2, but we can easily make it in time for 2025 wave 2 update 1 (27.1), which releases just a month later. I'm happy to do the backport for that one. |
src/System Application/Test/MicrosoftGraph/src/GraphClientTest.Codeunit.al
Show resolved
Hide resolved
src/System Application/Test/MicrosoftGraph/src/MockHttpClientHandlerMulti.Codeunit.al
Outdated
Show resolved
Hide resolved
|
@darjoo Thank you for your code review! How should one proceed in such cases when this exact approach is already used in the same module? And what about when it’s used across the entire application? Should I follow the same style as in the existing module? Should I then fix all the places in the module where it’s written the same way? In the application? Won’t this become a problem, with PRs being cluttered by these changes? I often have the urge to improve something, but I try to refrain from making unnecessary changes that are not directly related to the PR. For example, those two places you pointed out, I would have done them differently if I hadn’t seen this specific approach in the existing module. I’m genuinely curious how to handle such situations in order to maximize both quality and convenience for code review. |
|
@Drakonian Ah, I have not look at the existing CUs in the module so I didn't know. As we get new tools, processes change/improve etc, there will always be technical debt. It would be great if you could update them, however, I do not want to delay this PR as those are changes that aren't detrimental to the design/workings of the module, they are also test related so feel free to just close my comments. For resources, it's a nice tool to use and I'm frankly not sure how widely spread the tool is at this time and we can keep it as how it is. It's a nice to use, not a must use. Onto your question of how to handle such situations, if it's a matter of the design/workings of the module that makes it better, changing how the new implementation is done would be the best way imo. Whereas if it's a matter of "preference"/"style" I'd like changes but until we get an official style guide, keeping to the same style in the existing module is fine. Thanks for contributions! |
…m/Drakonian/BCApps into PaginationForMicrosoftGraphModule
|
That should do! Let's get this one in and ship it with 27.0 🥳 |
Summary
Pagination for MicrosoftGraph module
I will provide more examples and tests soon
Work Item(s)
Fixes #3926
Fixes AB#597634