A good example on when to use $expand when querying the Microsoft Graph

There are often situations where you may want to query the Microsoft Graph for certain stuff, such as any person that is a manager for someone. Let’s have a look on ways to that, and then how we can improve it.

Recently a script popped up in my LinkedIn feed, where the scripts indeed fetches all users that are managers for someone. Now, let it be said that as long as the script works for your use case – go for it! But let’s analyze the script a bit with runtime in mind.

The below is a very simplified version of the script:

# 1. Get all users from the Graph, but keep only the id 
$users = Get-MgUser -All | Select-Object Id

# 2. Find all users that are managers by checking whether they have direct reports
$managers = $users | Where-Object {
    Get-MgUserDirectReport -UserId $_.Id
}

# 3. Get the details of the managers
$managerDetails = $managers | ForEach-Object {
    Get-MgUser -UserId $_.Id | Select-Object Id, DisplayName, Mail, UserPrincipalName
}

This script will essentially:

  1. Get all users in the tenant from the Graph, including the id, displayname, mail and userprincipalname
    • But then we select to keep only the id
  2. For each user, send a request to get the direct reports of the user, to check whether they are a manager or not
  3. For each user that had direct reports, send a request to get all the required attributes

First of all, we could optimize this a bit by not selecting away the attributes under #1, simply by doings this:

# 1. Get all users from the Graph, but keep only the id 
$users = Get-MgUser -All -Property id, displayname, mail, userprincipalname

# 2. Find all users that are managers by checking whether they have direct reports
$managerDetails = $users | Where-Object {
    Get-MgUserDirectReport -UserId $_.Id
}

The result is the same, but we do not need to get the managers again. However, while we do reduce the number of requests sent to the Graph, we will need to get direct reports of each and every user in the tenant. This means that if you have 60 000 users, you need to send 60 000 of these requests. Assuming you can do 3 requests per second, this is almost 6 hours of run time.

So, what can we do to improve?

The Graph contains a lot of features that can help us out, such as the $expand option! Let’s see what we can do with that?

First of all, we can get a list of direct reports when getting users:

Or we can get the manager along with the user:

We can use the Graph SDK as well, and check the speed of both:

$c1 = Measure-Command {
    $usersWithManager = Get-MgUser -PageSize 999 -All -ExpandProperty manager
}

$c2 = Measure-Command {
    $usersWithDirectReports = Get-MgUser -PageSize 999 -All -ExpandProperty directReports
}

Write-Host "Manager to $($c1.TotalSeconds)"
Write-Host "DirectReports to $($c2.TotalSeconds)"

In my tenant with 67,680 users, this took 142 seconds for expanding manager, and 117 seconds for direct reports. That is actually the opposite of what I thought, as I was sure that expanding manager would be way faster, since direct reports are multi valued.

Now, from this we can either go through directReports:

$usersWithDirectReports = Get-MgUser -PageSize 999 -All -ExpandProperty directReports
$usersWithDirectReports | Where-Object directReports | Measure-Object

# 923 users

Or using the manager field:

$usersWithManager = Get-MgUser -PageSize 999 -All -ExpandProperty manager
$usersById = $usersWithManager | Group-Object id -AsHashTable

$usersWithManager | 
Where-Object Manager | 
ForEach-Object {$_.Manager.id} | 
Sort-Object -Unique | 
ForEach-Object {$usersById[$_]} | 
measure

# 923 users

So, the total runtime is way faster, between 2 and 3 minutes, and it is also way simplier than the original code that we started with, just because we utilized the expand feature.

So, hope that helps someone 🙂