The graph API returns for several kind of request (User, Groups, etc.) a page. This element contains the first bunch of elements and the NextPageRequest
property that can be used to make the next GetAsync()
call.
To make these things a little bit easier, also the PageIterator<T>
is available to get as much elements as needed. Before this we had a bunch of helper extension methods, that lacks this generic approach and are implemented for each kind of type seperately more or less like this:
public static async Task<IEnumerable<DirectoryObject>> GetAll(this Task<IGroupMembersCollectionWithReferencesPage> collectionTask)
{
// Argument checks, error handling and parameter for max item count omitted for better readability
var collectionPage = await collectionTask;
var start = (IEnumerable<DirectoryObject>)(collectionPage);
while (collectionPage.NextPageRequest != null)
{
collectionPage = await collectionPage.NextPageRequest.GetAsync();
start = start.Concat(collectionPage);
}
return start;
}
These kind of extension methods allowed to write something like this:
var currentGroupMembers = await serviceClient.Groups[azureGroup.Id].Members.Request().GetAsync().GetAll();
Due to the fact, that each kind of type returns its own type of ...Collection...Page
we had to write the same method and just replacing the given types.
Now PageIterator<T>
comes into play and we tried to write a generic function that works on all Task<ICollectionPage<TItem>>
. While it is quite easy to write such a function:
public static async Task<IReadOnlyList<TItem>> GetAllWithPageIterator<TItem>(this Task<ICollectionPage<TItem>> pageTask, IBaseClient baseClient)
{
// Argument checks, error handling and parameter for max item count omitted for better readability
var page = await pageTask;
var allElements = new List<TItem>();
var iterator = PageIterator<TItem>.CreatePageIterator(baseClient, page, item =>
{
allElements.Add(item);
return true;
});
await iterator.IterateAsync();
return allElements;
}
You can simply not use it:
// Extension method explicitly called to better show error message.
var task = serviceClient.Groups[azureGroup.Id].Members.Request().GetAsync();
await IGraphServiceCollectionPageExtensions.GetAllWithPageIterator<DirectoryObject>(task, serviceClient);
Argument 1: cannot convert from 'System.Threading.Tasks.Task<Microsoft.Graph.IGroupMembersCollectionWithReferencesPage>' to 'System.Threading.Tasks.Task<Microsoft.Graph.ICollectionPage<Microsoft.Graph.DirectoryObject>>'
But IGroupMembersCollectionWithReferencesPage
implements Microsoft.Graph.ICollectionPage<Microsoft.Graph.DirectoryObject>
, but I think the reason is the outer type Task<T>
avoids the usage as generic extension. If you rewrite the above extension method to use only the ICollectionPage<T>
everything works as expected. But that would break up the extension method to be always used as a two liner:
var firstPage = await serviceClient.Groups[azureGroup.Id].Members.Request().GetAsync();
var allItems = firstPage.GetAllWithPageIterator(firstPage, serviceClient);
While this works, it just looks bad to me (also if you would write (await serviceClient...GetAsync()).GetAll...
). It just looks like not being really matching and a real fluent solution like GetAsync().GetAll()
with generic constraints would be great.
While the question was built on v4 Graph SDK, we made a solution built on v5 Graph SDK. The problem about not having a shared base class or interface that can be used to implement one generic extension still exists. But the code would be all the time the same for each type and this sounds to be a good job for text templates:
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="NetStandard" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a text template.
// Creation date: <#= DateTime.Now #>
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Graph.Models;
namespace Microsoft.Graph
{
public static class GraphServiceCollectionResponseExtensions
{
<#
var pageTypes = new List<string>
{
"Group",
"User",
// Add further needed types here
};
foreach (var type in pageTypes)
{
#>
public static async Task<IReadOnlyList<<#= type #>>> GetAll(
this Task<<#= type #>CollectionResponse> collectionTask,
GraphServiceClient serviceClient,
int maxCount = int.MaxValue)
{
if (collectionTask == null)
throw new ArgumentNullException(nameof(collectionTask));
var response = await collectionTask;
var items = new List<<#= type #>>();
var iterator = PageIterator<<#= type #>, <#= type #>CollectionResponse>
.CreatePageIterator(
serviceClient,
response,
item =>
{
items.Add(item);
return items.Count < maxCount;
});
await iterator.IterateAsync();
return items;
}
<#
}
#>
}
}
By using this template you get an extension method for Group
s and User
s. If you need to get all pages from some other type, just add it to the pageTypes
list within the template. Some types that already have been tested are DriveItem
, Event
, Room
or ManagedDevice
.
The resulting code for e.g. users would look like this:
public static async Task<IReadOnlyList<User>> GetAll(
this Task<UserCollectionResponse> collectionTask,
GraphServiceClient serviceClient,
int maxCount = int.MaxValue)
{
if (collectionTask == null)
throw new ArgumentNullException(nameof(collectionTask));
var response = await collectionTask;
var items = new List<User>();
var iterator = PageIterator<User, UserCollectionResponse>
.CreatePageIterator(
serviceClient,
response,
item =>
{
items.Add(item);
return items.Count < maxCount;
});
await iterator.IterateAsync();
return items;
}
And to use it in your code you can take the following example:
var users = await serviceClient.Users.GetAsync().GetAll();