Last week I fell into a Graph REST API pitfall. In a customer project we implemented an interface that abstracts the interaction with Azure Active Directory (AAD). It defines a method that loads all users of a specific AAD group. The implementation of this interface interacts with Microsoft Graph REST API using GraphServiceClient
. The code looked like this (log statements removed for better readability):
public async Task<IEnumerable<Core.Models.User>> GetGroupUsersAsync(string groupName)
{
if (groupName == null)
{
throw new ArgumentNullException(nameof(groupName));
}
var graphServiceClient = GetGraphServiceClient();
var groups = await graphServiceClient
.Groups
.Request()
.Filter($"startsWith(displayName,'{groupName}')")
.Expand("Members")
.GetAsync();
var group = groups.FirstOrDefault();
if (group == null)
{
throw new GroupNotFoundException();
}
var users = group.Members.ToList();
var usersDetails = mapper.Map<IList<Core.Models.User>>(users.OfType<User>().ToList());
return usersDetails;
}
private GraphServiceClient GetGraphServiceClient()
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.CreateWithApplicationOptions(confidentialClientAppOptions.Value)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
return new GraphServiceClient(authProvider);
}
The Graph API REST request queries all groups (see here) starting with the provided group name and expands its members. The members of type User
are then returned to the caller. This implementation worked properly until more than 20 users got assigned to the corresponding group. Since the group consists mainly of guest users, our first guess was that some of them had not yet accepted the invitation and were therefore not returned. However, an initial analysis showed that there were also some users who were not returned even though they had already accepted the invitation.
During implementation we unfortunately didn’t recognize the note concerning expand query parameter which states, that expand is limited to 20 items (see here). Of course we would have noticed this if we had tested with more test data – a friendly reminder not to test with little data. As expanded items do not have an OData next link, we ended up with the following implementation after some research.
public async Task<IEnumerable<Core.Models.User>> GetGroupUsersAsync(string groupName)
{
if (groupName == null)
{
throw new ArgumentNullException(nameof(groupName));
}
var groups = await graphServiceClient
.Groups
.Request()
.Filter($"displayName eq '{groupName}'")
.Select(g => new { g.Id, g.DisplayName })
.GetAsync();
var group = groups.FirstOrDefault();
if (group == null)
{
throw new GroupNotFoundException();
}
var groupMembersCollection = await graphServiceClient
.Groups[group.Id]
.Members
.Request()
.Select("id,displayName,mobilePhone")
.GetAsync();
var users = groupMembersCollection.OfType<User>().ToList();
while (groupMembersCollection.NextPageRequest != null &&
(groupMembersCollection = await groupMembersCollection.NextPageRequest.GetAsync()).Count > 0)
{
users.AddRange(groupMembersCollection.OfType<User>().ToList());
}
var usersDetails = mapper.Map<IList<Core.Models.User>>(users);
return usersDetails;
}
The new implementation includes several improvements in addition to the fix. First, only Id
and DisplayName
of groups that exactly match the provided groupName
are requested from Microsoft Graph API. The use of the select
parameter reduces the response payload and therefore brings a performance improvement.
Next, a second request is sent to Microsoft Graph API which requests id
, displayName
and mobilePhone
(exclusively the required properties) of the members of the corresponding group. As members are of type `DirectoryObject` and therefore not only of type user, the result gets filtered by type User
.
Last but not least, possible additional items are requested by following the OData next links.
Hint: while playing around with Microsoft Graph Explorer, I realized that response payloads could get very large depending on the request. To avoid performance issues, I recommend using the select
parameter to reduce response payload to the required properties.
Leave a Reply