Best practices, tips and tricks working with Microsoft Graph API in PowerShell

Intro

Microsoft has also released a best practices blog. My best practices sometimes match Microsoft’s, but are my own ideas behind the best practices.
You don’t agree with one of the tips or best practices? Please leave feedback at the bottom of the post.

Microsoft own best practices blog:
Best practices for working with Microsoft Graph – Microsoft Graph | Microsoft Docs

I am using my own Optimized.Mga module as an example for the Best practices, tips and tricks for Microsoft Graph Rest API.


Best Practices


Always get all the data that can be gotten in one request, first

I think that this is one of the most important things there is. This creates a significant difference in speed and requests that are sent to the Microsoft Graph API.

Suppose you want to merge two data objects and make one report.
For example, I got a question from a customer asking if I could report the Mailbox and OneDrive sizes.

My most logical thought would be to get the Mailbox data first and then go through the users to get the OneDrive sizes and add it to a List.


Bad Example

So the PS script would look like this:


Connect-Mga -ApplicationID X  -Tenant X -ClientSecret X
$MBXURL = "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')"
$MBXSizes = Get-Mga -URL $MBXurl
$FusedDataSources = [System.Collections.Generic.List[System.Object]]::new()
foreach ($MBX in $MBXSizes) {
    $CurrentSP = $null
    $SPURL = "https://graph.microsoft.com/v1.0/users/{0}/drive" -f $MBX.'User Principal Name'
    try {
        $CurrentSP = Get-Mga -URL $SPURL 
        if ($null -ne $CurrentSP) {
            $Object = [PSCustomObject]@{
                UserPrincipalName = $MBX.'User Principal Name'
                MBSize   = $MBX.'Storage Used (Byte)'
                ODSize  = $CurrentSP.quota.used
            }
            $FusedDataSources.Add($Object)
        }
        else {
            $Object = [PSCustomObject]@{
                UserPrincipalName = $MBX.'User Principal Name'
                MBSize   = $MBX.'Storage Used (Byte)'
                ODSize  = 'No OneDrive found'
            }
            $FusedDataSources.Add($Object)
        }
    }
    catch {
        continue
    }
}

I use Fiddler to monitor the requests.
Let me list everything:

  • 1 Login to Graph.
  • 1 request for all the mailboxes including the size.
  • 999 requests to get the OneDrive size per user.
  • Measure-Command: 3 minutes and 8 seconds.
  • And lots of error messages because not all users have OneDrive.
The 404 errors in Fiddler are OneDrives that cannot be found. I had to use a try and catch my script to continue despite the error.

This means:

  • That the requests to Microsoft Graph increase quickly when you have more users.
  • This is a lot slower because every time a new request is made per user.
  • You also hit the throttle limit much earlier.

Fortunately, the throttling is handled in my module, but you want to hit it as little as possible.

Good example

Instead of requesting the OneDrive size per user, we first retrieve the sizes of all mailboxes and all OneDrives and then link them via a hashtable.

The script would look like this:

Connect-Mga -ApplicationID X  -Tenant X -ClientSecret X
$MBXURL = "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')"
$SPURL = "https://graph.microsoft.com/v1.0/reports/getOneDriveUsageAccountDetail(period='D7')"
$MBXSize = Get-Mga -URL $MBXurl
$SPSize = Get-Mga -URL $SPURL
$SPHash = @{}
foreach ($user in $SPSize)
{
    $SPHash.Add($user.'Owner Principal Name', $user)
}
$FusedDataSources = [System.Collections.Generic.List[System.Object]]::new()
foreach ($MBX in $MBXSize)
{
    $CurrentSP = $null
    $CurrentSP = $SPHash[$MBX.'User Principal Name']
    if ($null -ne $CurrentSP) {
        $Object = [PSCustomObject]@{
            UserPrincipalName = $MBX.'User Principal Name'
            MBSize   = $MBX.'Storage Used (Byte)'
            ODSize  = $CurrentSP.'Storage Used (Byte)'
        }
        $FusedDataSources.Add($Object)
    }
    else {
        $Object = [PSCustomObject]@{
            UserPrincipalName = $MBX.'User Principal Name'
            MBSize   = $MBX.'Storage Used (Byte)'
            ODSize  = 'No OneDrive found'
        }
        $FusedDataSources.Add($Object)
    }
}

To show you the difference, I ran this script on a customer environment where we implemented the script, and it run for 24883 users in 28 seconds. I wont even try this for the bad example.

  • 1 Login to Graph
  • 1 request for all the mailboxes including the size
  • 1 requests to get the OneDrive sizes

28 seconds runtime for 24883 users!


Conclusion

  • This means that for 24883 users it is 24878 less requests. The more users you have in your tenant, the bigger these ratios become. This makes a huge difference.
  • The script runtime becomes significantly less.
  • The chance that you hit the throttle limit is much smaller.

Make your request Uri as specific as possible

To prevent throttling, Microsoft has released a table in which you can see how much a request counts towards the throttling limit.
The table can be found here.

The table states that you should make the request as specific as possible.

Other factors that affect a request cost:
Using $select decreases cost by 1

They mean that you should only request the properties that you need.

Let’s take as an example that HR asks for all users in the tenant.
We can use the List Users request.

When we retrieve one user, you will get the following properties:

Microsoft Graph List Users returned properties

There are properties that are of no use for HR.
The displayName, givenName, surname, and mail should suffice.

With the query $select you can filter the properties.

The base URL with cmdlet would look like this:

Get-Mga -URL 'https://graph.microsoft.com/v1.0/users'

Queries are placed after the base URL.
When you build the first query you always start with a ‘?’ and then enter the “$select=’Property’, ‘Property2′”. (When you add a second query like ‘$filter’ you use & in stead of ‘?’)

So for this request it should look like this:

Get-Mga -URL 'https://graph.microsoft.com/v1.0/users?$select=displayName,givenName,surname,mail'

When we use Get-Mga with the new URL only these properties are retrieved:

Get-Mga -URL ‘https://graph.microsoft.com/v1.0/users?$select=displayName,givenName,surname,mail’

This saves 1 count on the throttle limit.
If we run this for 24883 users, this will make a huge difference.


Use batch requests when you have multiple requests

If you need to create multiple users, or change a property for multiple users, you can do this via Post or Put method, or you can do this with a Batch request.

With a Batch request you can send multiple requests at once, which means that this counts as one request.
The advantage of this is that:

  1. It is much faster than a normal put- or Post-Mga.
  2. You are less likely to hit the throttle limit.

You can still hit a throttle and the batch request can receive a maximum of 20 requests at a time.


Batch-Mga to the rescue

I have processed all of this in Batch-Mga.
You have no maximums with this cmdlet and in theory you can even create 10,000 users via Batch-Mga without any problems.

I’ve done this before as an example for Github.
For more details see my Github for Optimized.Mga module.

In short; I created 10.000 users in 10 minutes.

Both Patch- and Delete-Mga can use Batching in the backend.


Try not to use Where-Object in your foreach loop, but match your data with hashtables

In the first best practice tip I already give a hint, but hashtables make a huge difference in speed when you match data. I’m going to use the same example as before. This tip also counts outside of Microsoft Graph API.

I have shortened the scripts below, the bad example ran for only 1000 users and the good example ran for 24883 users.

  • $MBXSize contains the Mailbox sizes.
  • $SPSize contains the OneDrive sizes.

Bad Example

foreach ($MBX in $MBXSize) {
    $CurrentSP = $null
    $CurrentSP = $SPsize | Where-Object { $_.'Owner Principal Name' -eq $MBX.'User Principal Name' } 
    if ($null -ne $CurrentSP) {
        $Object = [PSCustomObject]@{
            UserPrincipalName = $MBX.'User Principal Name'
            MailboxSizeInGb   = $MBX.'Storage Used (Byte)'
            OneDriveSizeInGb  = $CurrentSP.'Storage Used (Byte)'
        }
        $FusedDataSources.Add($Object)
    }
    else {
        $Object = [PSCustomObject]@{
            UserPrincipalName = $MBX.'User Principal Name'
            MailboxSizeInGb   = $MBX.'Storage Used (Byte)'
            OneDriveSizeInGb  = 'No OneDrive found'
        }
        $FusedDataSources.Add($Object)
    }
}

Unfortunately it took way too long to run for 24883 users. So I had to shorten it to 1000 users.

The data matched in 13 minutes.

Measure-Command: 13 minutes and 45 seconds

Good Example

$SPHash = @{}
foreach ($user in $SPSize) {
    $SPHash.Add($user.'Owner Principal Name', $user)
}
$FusedDataSources = [System.Collections.Generic.List[System.Object]]::new()
foreach ($MBX in $MBXSize) {
    $CurrentSP = $null
    $CurrentSP = $SPHash[$MBX.'User Principal Name']
    if ($null -ne $CurrentSP) {
        $Object = [PSCustomObject]@{
            UserPrincipalName = $MBX.'User Principal Name'
            MBSize   = $MBX.'Storage Used (Byte)'
            ODSize  = $CurrentSP.'Storage Used (Byte)'
        }
        $FusedDataSources.Add($Object)
    }
    else {
        $Object = [PSCustomObject]@{
            UserPrincipalName = $MBX.'User Principal Name'
            MBSize   = $MBX.'Storage Used (Byte)'
            ODSize  = 'No OneDrive found'
        }
        $FusedDataSources.Add($Object)
    }
}

The data matched in less than 2 seconds.

Measure-Command: 1 Seconds and 794 miliseconds

Conclusion

USE HASH TABLES!


Tips and tricks for Microsoft Graph Rest API


Graph Explorer

The Graph Explorer is one of the easiest ways to find out if your URL works without needing the correct permissions for your App registration.

You can paste a URL here and then see what the result is without having to log in to an environment. The Graph Explorer will then automatically display a demo environment.

Use the Graph Explorer to test your URLs and see if they work
Use the Graph Explorer to test your URLs and see if they work

I prefer to work in a demo Office 365 tenant with which I log in to the graph explorer to test my URLs. This way you get the most out of it.

For example when retrieving a user account with certain properties and so on. You can even use the Put, Post, Patch, and Delete methods when you’re logged in. Keep in mind though that these Methods will actually run on your tenant.

The Put, Post, Patch, and Delete methods will run on your tenant
The Put, Post, Patch, and Delete methods will run on your tenant

Use Fiddler

Fiddler is a web debugging proxy to monitor your device network traffic.

Fiddler also shows the requests you send to Microsoft Graph API.
The below screenshot is an example of a request to retrieve an Oauth token.

  1. Is the request
  2. Is the header
  3. This is the response
    Here you can see the Oauth token including the validity (1 hour)
Conect-Mga to retrieve an oauth token for Microsoft Graph API
Conect-Mga to retrieve an oauth token for Microsoft Graph API in Fiddler

Fiddler use case

Why Fiddler? Well, I have a nice use case for you why.

I used the Patch-Mga cmdlet to add users to a group.
This was a script that was running fine for a while, so strange that I suddenly got the following error in PowerShell.

The remote server returned an error: (400) Bad Request.

Looking at the HTTP status codes it states this:

400Bad RequestCannot process the request because it is malformed or incorrect.
The remote server returned an error: (400) Bad Request.

You can do little with such an error message.
How is it possible that it has worked for awhile and suddenly no longer?

I started up Fiddler and pressed F12 to capture traffic.
After that I ran the script again and saw this error in Fiddler:

Best practices, tips and tricks working with Microsoft Graph API in PowerShell
Best practices, tips and tricks working with Microsoft Graph API in PowerShell

I usually filter on the process, to only see events from my PowerShell console.
Right click on the event > Filter now > Show only process=123456

Best practices, tips and tricks working with Microsoft Graph API in PowerShell
Best practices, tips and tricks working with Microsoft Graph API in PowerShell

Double click the event and open the Raw tab.
Here you see the full error message:

"message": "A resource cannot contain more than '20' link changes.",

In PowerShell you get HTTPS status code and in Fiddler you see the error message. (It is possible in PowerShell, but this differs per error message, making it difficult to filter this.)

Due to the above error message I have adjusted Patch- and Batch-Mga to group them by 20 with more than 20 requests.


Use a Certificate to authenticate

There are several ways to receive an Oauth token for Microsoft Graph. The most secure way is a certificate and it is not that difficult.

I sometimes see scripts at customers, on servers where multiple suppliers have access to, with a password in plain text and the same goes for ClientSecrets. A ClientSecret is like a password.

Follow these steps to secure your App registrations with a certificate and remove your ClientSecrets.

You can then retrieve the Oauth token with Connect-Mga -Certificate or -Thumbprint.


Always try least to most privileges

Use the minimum permissions required for your script and do not reuse app registrations for different scripts to keep the minimum permissions.

If you look at the List Users Graph information page, one of the first headers is the permissions tab. It show the permissions from least to most privileges.

Best practices, tips and tricks working with Microsoft Graph API in PowerShell
Best practices, tips and tricks working with Microsoft Graph API in PowerShell

The permissions are unfortunately not always updated correctly by Microsoft. Due to this it can happen that your request still doesn’t work.

Then try the URL with the Graph Explorer. On the Graph Explorer page after you test a URL, the Modify Permissions tab will also show the permissions from least to most privilege.

Best practices, tips and tricks working with Microsoft Graph API in PowerShell
Best practices, tips and tricks working with Microsoft Graph API in PowerShell

Credits for Maurice Lok-Hin

Credits go to Maurice Lok-Hin who has taught me this over the last year.
Due to him I was able to reduce scripts from 5 to 6 hours back to 20 to 30 minutes.

Published by

Bas Wijdenes

My name is Bas Wijdenes and I work as a PowerShell DevOps Engineer. In my spare time I write about interesting stuff that I encounter during my work.

8 thoughts on “Best practices, tips and tricks working with Microsoft Graph API in PowerShell”

  1. measure-command {get-mguser -Property id,userPrincipalName,lastPasswordChangeDateTime,createdDateTime,PasswordPolicies -all}

    Days : 0
    Hours : 0
    Minutes : 0
    Seconds : 9
    Milliseconds : 516
    Ticks : 95162940
    TotalDays : 0.000110142291666667
    TotalHours : 0.002643415
    TotalMinutes : 0.1586049
    TotalSeconds : 9.516294
    TotalMilliseconds : 9516.294

    $Uri = ‘https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,lastPasswordChangeDateTime,createdDateTime,PasswordPolicies’
    measure-command {Get-Mga -Uri $Uri -Api ‘All’}

    Days : 0
    Hours : 0
    Minutes : 0
    Seconds : 14
    Milliseconds : 263
    Ticks : 142632986
    TotalDays : 0.000165084474537037
    TotalHours : 0.00396202738888889
    TotalMinutes : 0.237721643333333
    TotalSeconds : 14.2632986
    TotalMilliseconds : 14263.2986

    1. You should use top=999 or something in the url:
      $Uri = ‘https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,lastPasswordChangeDateTime,createdDateTime,PasswordPolicies&$top=999’

  2. getOneDriveUsageAccountDetail not working for me v1.0 in C#
    var unsortedUsersCollection = graphServiceClient.Reports.GetOneDriveUsageAccountDetail(“D180”).Request(queryOptionsN).GetAsync().Result;

    InnerException = {“‘R’ is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.”}

  3. Hi, first of all thanks for all the information and the powershell module, its very usefull.
    i have kind of newbie question i tried to create users or users using your example of batch-mga or mga-post and cant get the $users or $CreatedUsers format right.
    any chance you can post whats inside these variables?
    Thanks in advance

    1. Hi Turbo,
      Thank you for your reply.

      Below is an example of one user in the $CreatedUsers array.
      Did you get it from the Github site? I’ll update it there as well.

      userPrincipalName : [email protected]
      displayName : DkFZxshunN
      accountEnabled : true
      mailNickname : DkFZxshunN
      passwordProfile : @{password=H78302ehpib; forceChangePasswordNextSignIn=true}

      EDIT: Updated on Github as well.

      1. sorry for late reply thanks
        I revisited this module now and was testing around 10k users using regular get-mguser compared to getmga and i don’t understand why i am getting 9 sec+- on get-mga compared to get-mguser?
        isn’t it supposed to be much faster?
        i verified i am retrieving all users and the same attributes on both so:
        $Uri = ‘https://graph.microsoft.com/v1.0/users?$select=id,userPrincipalName,lastPasswordChangeDateTime,createdDateTime,PasswordPolicies’
        measure-command {Get-Mga -Uri $Uri -Api ‘All’}
        compared to the regular:
        measure-command {get-mguser -Property id,userPrincipalName,lastPasswordChangeDateTime,createdDateTime,PasswordPolicies -all}

        thanks again

Leave a Reply

Your email address will not be published. Required fields are marked *