I’ve written a couple of articles about using Microsoft Graph queries with PowerShell to access data that you can’t normally get to with cmdlets. For instance, this example explains how to report the somewhat bizarre email addresses assigned to Teams channels. When a channel is email-enabled, people can post to the channel by sending email to the assigned address, which is a good way to introduce information into Teams. Apart from being posted as new topics in the channel, messages are captured in the channel’s folder in the SharePoint document library belonging to the team.
Limited Data Returned by Design
But as a comment to the article notes, when you use the Invoke-WebRequest
cmdlet to send a Graph command to fetch information about the set of Teams in a
tenant, the Graph responds with 100 teams. This is what the Graph intends to do
because it doesn’t want the response to be too large. And the response is good
enough to prove the principle of working with the Graph through PowerShell.
However, once you get to production, you probably need to deal with more than
100 teams and need to be able to fetch all the teams in the tenant and process
them.
Processing Four Thousand Teams
A few days ago, Mike Tilson posted a note in the Office 365 Facebook group asking if it is “possible to run a report (hopefully via PowerShell) to report on what Microsoft Teams apps (third party apps like Polly) are installed across your environment?” As it happens, the esteemed technical editor for the Office 365 for IT Pros eBook, Vasil Michev, had written a script to report on apps and tabs. I took his script and made (in my mind) some improvements, and I gave Mike a copy of the script.
Mike’s response was that the script worked great in a small
environment but had 2,000+ teams (among 4,000-odd groups) to report on in
production. The code needed to be upgraded to process larger numbers.
NextLink is the Key
The solution is a thing called a nextlink, available when Microsoft Graph queries have more data to provide to clients than is returned to the original request (server-side paging). A page is the set of data returned for a Graph call and several pages might need to be retrieved to fetch the full set of objects you want to process The documentation says “When a result set spans multiple pages, Microsoft Graph returns an @odata.nextLink property in the response that contains a URL to the next page of results.” In effect, the nextlink tells the Graph the next set of data to return if an application wishes to request it following its first call. To be sure that you get all data, you need to fetch it page by page until the nextlink is null. This is called pagination.
If you use the Graph Explorer to play with Microsoft Graph queries, you’ll see that a nextlink turns up in calls like “all groups in my organization.” You know it’s a nextlink because it includes the term “skiptoken” as in https://graph.microsoft.com/v1.0/groups?$skiptoken=X%274453707402 (a real nextlink is much longer). Microsoft Graph queries can generate nextlinks after fetching less than 100 items. For instance, the default number of folders retrieved from a mailbox is 10.
Solving the Problem
The uprated code:
Makes the original call to fetch teams and stores the object identifier and display name for each team in a hashtable. A hashtable is suitable when you only need to store two properties. If you need to store more, create and populate a generic list instead.
Checks if a nextlink is returned.
If a nextlink is found, make another call to the Graph to fetch another set (page) of data and store those items in the hashtable. This call uses the nextlink to tell the Graph where to start retrieving data.
Continue until the nextlink is null, meaning that no more data remains to be fetched.
Here’s the code, based on an original solution created by Mike Tilson.
$Uri = "https://graph.microsoft.com/V1.0/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')"
[array]$Teams = Invoke-WebRequest -Method GET -Uri $Uri -ContentType "application/json" -Headers $Headers | ConvertFrom-Json
If ($Teams.Value.Count -eq 0) { Write-Host "No Teams found - exiting!"; break }
$Teams.Value.ForEach( {
$TeamsHash.Add($_.Id, $_.DisplayName) } )
$NextLink = $Teams.'@Odata.NextLink'
While ($NextLink -ne $Null) {
$Teams = Invoke-WebRequest -Method GET -Uri $NextLink -ContentType $ctype -Headers $headers | ConvertFrom-Json
$Teams.Value.ForEach( {
$TeamsHash.Add($_.Id, $_.DisplayName) } )
$NextLink = $Teams.'@odata.NextLink' }
Get-Team Works Too
Another pragmatic solution to the problem of how to fetch
all teams in a tenant is to use the Get-Team cmdlet. Using Get-Team
is much slower than the code listed above (minutes instead of seconds), but the
cmdlet handles the paging for you.
After fetching the set of teams, we can begin to process each team to discover what apps and tabs are installed in it. The Graph calls in the script to fetch channels, tabs, and apps don’t use paging. A team can have up to 200 channels, so I guess the call to fetch channels might need to change to handle such a well-endowed (and possibly confusing) team.
Still Work to Do
Although we can now fetch all the teams in a tenant, things aren’t perfect yet. I found a problem processing archived teams, where the attempt to retrieve app information fails. I can’t see how to identify an archived team from the information returned, so more research is needed. (Update: check the isArchived property in team settings to find archived teams).
Another issue is that the auth token expires after an hour and stops the script. A refresh token is needed at this point. (Update: the latest version of the Teams and Groups Report script illustrates how to renew an access token).
If you’d like to improve the code and make it even better, you can get the script from GitHub. In the meantime, enjoy using Microsoft Graph queries to process data with PowerShell.
Need help understand how to use PowerShell to manage Office 365 Groups and Teams? The Office 365 for IT Pros eBook contains a ton of examples to help you get going.
{"id":null,"mode":"button","open_style":"in_modal","currency_code":"EUR","currency_symbol":"\u20ac","currency_type":"decimal","blank_flag_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/blank.gif","flag_sprite_url":"https:\/\/office365itpros.com\/wp-content\/plugins\/tip-jar-wp\/\/assets\/images\/flags\/flags.png","default_amount":100,"top_media_type":"featured_image","featured_image_url":"https:\/\/office365itpros.com\/wp-content\/uploads\/2022\/11\/cover-141x200.jpg","featured_embed":"","header_media":null,"file_download_attachment_data":null,"recurring_options_enabled":true,"recurring_options":{"never":{"selected":true,"after_output":"One time only"},"weekly":{"selected":false,"after_output":"Every week"},"monthly":{"selected":false,"after_output":"Every month"},"yearly":{"selected":false,"after_output":"Every year"}},"strings":{"current_user_email":"","current_user_name":"","link_text":"Virtual Tip Jar","complete_payment_button_error_text":"Check info and try again","payment_verb":"Pay","payment_request_label":"Office 365 for IT Pros","form_has_an_error":"Please check and fix the errors above","general_server_error":"Something isn't working right at the moment. Please try again.","form_title":"Office 365 for IT Pros","form_subtitle":null,"currency_search_text":"Country or Currency here","other_payment_option":"Other payment option","manage_payments_button_text":"Manage your payments","thank_you_message":"Thank you for supporting the work of Office 365 for IT Pros!","payment_confirmation_title":"Office 365 for IT Pros","receipt_title":"Your Receipt","print_receipt":"Print Receipt","email_receipt":"Email Receipt","email_receipt_sending":"Sending receipt...","email_receipt_success":"Email receipt successfully sent","email_receipt_failed":"Email receipt failed to send. Please try again.","receipt_payee":"Paid to","receipt_statement_descriptor":"This will show up on your statement as","receipt_date":"Date","receipt_transaction_id":"Transaction ID","receipt_transaction_amount":"Amount","refund_payer":"Refund from","login":"Log in to manage your payments","manage_payments":"Manage Payments","transactions_title":"Your Transactions","transaction_title":"Transaction Receipt","transaction_period":"Plan Period","arrangements_title":"Your Plans","arrangement_title":"Manage Plan","arrangement_details":"Plan Details","arrangement_id_title":"Plan ID","arrangement_payment_method_title":"Payment Method","arrangement_amount_title":"Plan Amount","arrangement_renewal_title":"Next renewal date","arrangement_action_cancel":"Cancel Plan","arrangement_action_cant_cancel":"Cancelling is currently not available.","arrangement_action_cancel_double":"Are you sure you'd like to cancel?","arrangement_cancelling":"Cancelling Plan...","arrangement_cancelled":"Plan Cancelled","arrangement_failed_to_cancel":"Failed to cancel plan","back_to_plans":"\u2190 Back to Plans","update_payment_method_verb":"Update","sca_auth_description":"Your have a pending renewal payment which requires authorization.","sca_auth_verb":"Authorize renewal payment","sca_authing_verb":"Authorizing payment","sca_authed_verb":"Payment successfully authorized!","sca_auth_failed":"Unable to authorize! Please try again.","login_button_text":"Log in","login_form_has_an_error":"Please check and fix the errors above","uppercase_search":"Search","lowercase_search":"search","uppercase_page":"Page","lowercase_page":"page","uppercase_items":"Items","lowercase_items":"items","uppercase_per":"Per","lowercase_per":"per","uppercase_of":"Of","lowercase_of":"of","back":"Back to plans","zip_code_placeholder":"Zip\/Postal Code","download_file_button_text":"Download File","input_field_instructions":{"tip_amount":{"placeholder_text":"How much would you like to tip?","initial":{"instruction_type":"normal","instruction_message":"How much would you like to tip? Choose any currency."},"empty":{"instruction_type":"error","instruction_message":"How much would you like to tip? Choose any currency."},"invalid_curency":{"instruction_type":"error","instruction_message":"Please choose a valid currency."}},"recurring":{"placeholder_text":"Recurring","initial":{"instruction_type":"normal","instruction_message":"How often would you like to give this?"},"success":{"instruction_type":"success","instruction_message":"How often would you like to give this?"},"empty":{"instruction_type":"error","instruction_message":"How often would you like to give this?"}},"name":{"placeholder_text":"Name on Credit Card","initial":{"instruction_type":"normal","instruction_message":"Enter the name on your card."},"success":{"instruction_type":"success","instruction_message":"Enter the name on your card."},"empty":{"instruction_type":"error","instruction_message":"Please enter the name on your card."}},"privacy_policy":{"terms_title":"Terms and conditions","terms_body":null,"terms_show_text":"View Terms","terms_hide_text":"Hide Terms","initial":{"instruction_type":"normal","instruction_message":"I agree to the terms."},"unchecked":{"instruction_type":"error","instruction_message":"Please agree to the terms."},"checked":{"instruction_type":"success","instruction_message":"I agree to the terms."}},"email":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email address"},"success":{"instruction_type":"success","instruction_message":"Enter your email address"},"blank":{"instruction_type":"error","instruction_message":"Enter your email address"},"not_an_email_address":{"instruction_type":"error","instruction_message":"Make sure you have entered a valid email address"}},"note_with_tip":{"placeholder_text":"Your note here...","initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"empty":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"not_empty_initial":{"instruction_type":"normal","instruction_message":"Attach a note to your tip (optional)"},"saving":{"instruction_type":"normal","instruction_message":"Saving note..."},"success":{"instruction_type":"success","instruction_message":"Note successfully saved!"},"error":{"instruction_type":"error","instruction_message":"Unable to save note note at this time. Please try again."}},"email_for_login_code":{"placeholder_text":"Your email address","initial":{"instruction_type":"normal","instruction_message":"Enter your email to log in."},"success":{"instruction_type":"success","instruction_message":"Enter your email to log in."},"blank":{"instruction_type":"error","instruction_message":"Enter your email to log in."},"empty":{"instruction_type":"error","instruction_message":"Enter your email to log in."}},"login_code":{"initial":{"instruction_type":"normal","instruction_message":"Check your email and enter the login code."},"success":{"instruction_type":"success","instruction_message":"Check your email and enter the login code."},"blank":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."},"empty":{"instruction_type":"error","instruction_message":"Check your email and enter the login code."}},"stripe_all_in_one":{"initial":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"empty":{"instruction_type":"error","instruction_message":"Enter your credit card details here."},"success":{"instruction_type":"normal","instruction_message":"Enter your credit card details here."},"invalid_number":{"instruction_type":"error","instruction_message":"The card number is not a valid credit card number."},"invalid_expiry_month":{"instruction_type":"error","instruction_message":"The card's expiration month is invalid."},"invalid_expiry_year":{"instruction_type":"error","instruction_message":"The card's expiration year is invalid."},"invalid_cvc":{"instruction_type":"error","instruction_message":"The card's security code is invalid."},"incorrect_number":{"instruction_type":"error","instruction_message":"The card number is incorrect."},"incomplete_number":{"instruction_type":"error","instruction_message":"The card number is incomplete."},"incomplete_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incomplete."},"incomplete_expiry":{"instruction_type":"error","instruction_message":"The card's expiration date is incomplete."},"incomplete_zip":{"instruction_type":"error","instruction_message":"The card's zip code is incomplete."},"expired_card":{"instruction_type":"error","instruction_message":"The card has expired."},"incorrect_cvc":{"instruction_type":"error","instruction_message":"The card's security code is incorrect."},"incorrect_zip":{"instruction_type":"error","instruction_message":"The card's zip code failed validation."},"invalid_expiry_year_past":{"instruction_type":"error","instruction_message":"The card's expiration year is in the past"},"card_declined":{"instruction_type":"error","instruction_message":"The card was declined."},"missing":{"instruction_type":"error","instruction_message":"There is no card on a customer that is being charged."},"processing_error":{"instruction_type":"error","instruction_message":"An error occurred while processing the card."},"invalid_request_error":{"instruction_type":"error","instruction_message":"Unable to process this payment, please try again or use alternative method."},"invalid_sofort_country":{"instruction_type":"error","instruction_message":"The billing country is not accepted by SOFORT. Please try another country."}}}},"fetched_oembed_html":false}
6 Replies to “Using Microsoft Graph API Queries to Process Large Amounts of Data”