Microsoft Graph REST API pitfall $expand

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

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Website Powered by WordPress.com.

Up ↑

%d bloggers like this: