He looks happy, but he hasn’t hit some of the SDK foibles yet…
Table of Contents
Translating Graph API Requests to PowerShell Cmdlets Sometimes Doesn’t Go So Well
The longer you work with a technology, the more you come to know about its strengths and weaknesses. I’ve been working with the Microsoft Graph PowerShell SDK for about two years now. I like the way that the SDK makes Graph APIs more accessible to people accustomed to developing PowerShell scripts, but I hate some of the SDK’s foibles.
Sometimes you just don’t want to write something into a property and that’s what PowerShell’s $Null variable is for. But the Microsoft Graph PowerShell SDK cmdlets don’t like it when you use $Null. For example, let’s assume you want to create a new Azure AD user account. This code creates a hash table with the properties of the new account and then runs the New-MgUser cmdlet.
New-MgUser fails because of an invalid value for the department property, even though $Null is a valid PowerShell value.
New-MgUser : Invalid value specified for property 'department' of resource 'User'.
At line:1 char:2
+ $NewGuestAccount = New-MgUser @NewUserProperties
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: ({ body = Micros...oftGraphUser1 }:<>f__AnonymousType64`1) [New-MgUser
_CreateExpanded], RestException`1
+ FullyQualifiedErrorId : Request_BadRequest,Microsoft.Graph.PowerShell.Cmdlets.NewMgUser_CreateExpanded
One solution is to use a variable that holds a single space. Another is to pass $Null by running the equivalent Graph request using the Invoke-MgGraphRequest cmdlet. Neither are good answers to what should not happen (and we haven’t even mentioned the inability to filter on null values).
Ignoring the Pipeline
The pipeline is a fundamental building block of PowerShell. It allows objects retrieve by a cmdlet to pass to another cmdlet for processing. But despite the usefulness of the pipeline, the SDK cmdlets don’t support it and the pipeline stops stone dead whenever an SDK cmdlet is asked to process incoming objects. For example:
Get-MgUser -Filter "userType eq 'Guest'" -All | Update-MgUser -Department "Guest Accounts"
Update-MgUser : The pipeline has been stopped
Why does this happen? The cmdlet that receives objects must be able to distinguish between the different objects before it can work on them. In this instance, Get-MgUser delivers a set of guest accounts, but the Update-MgUser cmdlet does not know how to process each object because it identifies an object is through the UserId parameter whereas the inbound objects offer an identity in the Id property.
The workaround is to store the set of objects in an array and then process the objects with a ForEach loop.
Property Casing and Fetching Data
I’ve used DisplayName to refer to the display name of objects since I started to use PowerShell with Exchange Server 2007. I never had a problem with uppercasing the D and N in the property name until the Microsoft Graph PowerShell SDK came along only to find that sometimes SDK cmdlets insist on a specific form of casing for property names. Fail to comply, and you don’t get your data.
What’s irritating is that the restriction is inconsistent. For instance, both these commands work:
This works, but I end up with a set of identifiers pointing to individual group members. Then I remember from experience gained from building scripts to report group membership that Get-MgGroupMember (like other cmdlets dealing with membership like Get-MgAdministrationUnitMember) returns a property called AdditionalProperties holding extra information about members. So I try:
$GroupMembers.AdditionalProperties.DisplayName
Nope! But if I change the formatting to displayName, I get the member names:
$GroupMembers.AdditionalProperties.displayName
Tony Redmond
Kim Akers
James Ryan
Ben James
John C. Adams
Chris Bishop
Talk about frustrating confusion! It’s not just display names. Reference to any property in AdditionalProperties must use the same casing as used the output, like userPrincipalName and assignedLicenses.
Another example is when looking for sign-in logs. This command works because the format of the user principal name is the same way as stored in the sign-in log data:
Two SDK foibles are on show here. First, the way that cmdlets return sets of identifiers and stuff information into AdditionalProperties (something often overlooked by developers who don’t expect this to be the case). Second, the inconsistent insistence by cmdlets on exact matching for property casing.
I’m told that this is all due to the way Graph APIs work. My response is that it’s not beyond the ability of software engineering to hide complexities from end users by ironing out these kinds of issues.
GUIDs and User Principal Names
Object identification for Graph requests depends on globally unique identifiers (GUIDs). Everything has a GUID. Both Graph requests and SDK cmdlets use GUIDs to find information. But some SDK cmdlets can pass user principal names instead of GUIDs when looking for user accounts. For instance, this works:
Unless you want to include the latest sign-in activity date for the account.
Get-MgUser -UserId Tony.Redmond@office365itpros.com -Property signInActivity
Get-MgUser :
{"@odata.context":"http://reportingservice.activedirectory.windowsazure.com/$metadata#Edm.String","value":"Get By Key
only supports UserId and the key has to be a valid Guid"}
The reason is that the sign-in data comes from a different source which requires a GUID to lookup the sign-in activity for the account, so we must pass the object identifier for the account for the command to work:
It’s safer to use GUIDs everywhere. Don’t depend on user principal names because a cmdlet might object – and user principal names can change.
No Fix for Problems in V2 of the Microsoft Graph PowerShell SDK
V2.0 of the Microsoft Graph PowerShell SDK is now in preview. The good news is that V2.0 delivers some nice advances. The bad news is that it does nothing to cure the weaknesses outlined here. I’ve expressed a strong opinion that Microsoft should fix the fundamental problems in the SDK before doing anything else.
I’m told that the root cause of many of the issues is the AutoRest process Microsoft uses to generate the Microsoft Graph PowerShell SDK cmdlets from Graph API metadata. It looks like we’re stuck between a rock and a hard place. We benefit enormously by having the SDK cmdlets but the process that makes the cmdlets available introduces its own issues. Let’s hope that Microsoft gets to fix (or replace) AutoRest and deliver an SDK that’s better aligned with PowerShell standards before our remaining hair falls out due to the frustration of dealing with unpredictable cmdlet behavior.
Insight like this doesn’t come easily. You’ve got to know the technology and understand how to look behind the scenes. Benefit from the knowledge and experience of the Office 365 for IT Pros team by subscribing to the best eBook covering Office 365 and the wider Microsoft 365 ecosystem.
9 Replies to “Mastering the Foibles of the Microsoft Graph PowerShell SDK”
Hey Tony!
For this example “[array]$Logs = Get-MgAuditLogSignIn -Filter “UserPrincipalName eq ‘james.ryan@office365itpros.com'” -All” there is a working solution that Microsoft use in the backend if we analyze the Graph requests during filtering through the portal:
Sorry, I made a mistake with parentheses. This is working example:
[array]Get-MgAuditLogSignIn -Filter “tolower(UserPrincipalName) eq ‘$($UPN.ToLower())'” -All
But I completely agree with you that it would be better not to have to do this for all Graph cmdlets filter expressions.
Loading...
Tony,
Great post (even though a little frustrating).
Regarding the ‘additional properties’ – I think those uppercase/lowercase issues existed even before, in the AzureAD PS module.
I often refer to the ‘get-azureaduser’ extensionProperty attribute. Among other additional details, it provides for the dirSynced objects the distinguishedName of the on-prem object. The name of that sub-attribute (within the ‘extensionProperty’ attribute) is ‘onPremisesDistinguishedName’ and it has to be written with capital ‘P’, ‘D’ and ‘N’ – otherwise it doesn’t work. The same stays for ‘createdDateTime’ sub-attribute, and probably with other sub-attributes as well (haven’t tested them all).
So even if the strange behavior for $null variable or no support for pipeline (which sounds weird, as in my opinion pipeline is one of the strongest powers of PS), sometimes not only Graph SDK is to blame.
I think the source of those problems is the Azure AD Graph… which is now being deprecated, but whose code and metadata advanced into the Microsoft Graph and is picked up by AutoREST when it generates the SDK cmdlets… Decisions taken years ago can have big consequences.
Well said. It’s obvious that in developing the Microsoft PowerShell Graph API Microsoft lost sight of the 80/20 rule – 80% of people only use 20% of the functionality of the software. In the old modules, the 20% that most people probably use (user management, group management, license management etc) worked fairly well, including things like pipeline support. Microsoft now move to Graph, and one of the clear benefits is that you can access nearly 100% of the API using the PowerShell cmdlets. But the problem is – *most people don’t care* – we just want the 20% that we need to work smoothly.
They would do well to focus on that ‘core functionality’ before trying to do more work on the stuff many people will touch rarely, if ever.
“The workaround is to store the set of objects in an array and then process the objects with a ForEach loop.”
That’s like…back to VBS era from 15 years ago ? Sounds insane to me…
This SDK reminds me of Visual Studio Code : a tool made by developers for developers, bloated with tons of useless stuff for admins and regressions (like a Windows 3.11 UI).
Admins need efficient, intuitive, consistent, modular, well-documented scripting tools. A lot of us do not use nor write apps for day-to-day tasks like querying account properties.
Just rewriting PS scripts to remove the pipeline will make them much more complex, long, and hard to maintain. There was a reason Powershell was a welcomed revolution so many years ago !
I really don’t understand how could it come to this. VBScript as a leading technology for Azure scripting in the next ten years at least, really ?
Sorry for the rant. It feels like Azure tooling is going backwards since the Graph stack showed up, which is very frustrating. We could use at least more and better communication from Microsoft about this.
{"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}
Hey Tony!
For this example “[array]$Logs = Get-MgAuditLogSignIn -Filter “UserPrincipalName eq ‘james.ryan@office365itpros.com'” -All” there is a working solution that Microsoft use in the backend if we analyze the Graph requests during filtering through the portal:
[array]$Logs = Get-MgAuditLogSignIn -Filter “(tolower(UserPrincipalName) eq ‘$($UPN.ToLower())”” -All
Unfortunately, Graph tolower filter query parameter is not working with all properties that we want to filter on.
It’s still an inconsistency because the bloody filter should work without manipulation…
BTW, did you paste the code in correctly? It didn’t work for me…. Get-MgAuditLogSignIn : Invalid filter clause
Sorry, I made a mistake with parentheses. This is working example:
[array]Get-MgAuditLogSignIn -Filter “tolower(UserPrincipalName) eq ‘$($UPN.ToLower())'” -All
But I completely agree with you that it would be better not to have to do this for all Graph cmdlets filter expressions.
Tony,
Great post (even though a little frustrating).
Regarding the ‘additional properties’ – I think those uppercase/lowercase issues existed even before, in the AzureAD PS module.
I often refer to the ‘get-azureaduser’ extensionProperty attribute. Among other additional details, it provides for the dirSynced objects the distinguishedName of the on-prem object. The name of that sub-attribute (within the ‘extensionProperty’ attribute) is ‘onPremisesDistinguishedName’ and it has to be written with capital ‘P’, ‘D’ and ‘N’ – otherwise it doesn’t work. The same stays for ‘createdDateTime’ sub-attribute, and probably with other sub-attributes as well (haven’t tested them all).
So even if the strange behavior for $null variable or no support for pipeline (which sounds weird, as in my opinion pipeline is one of the strongest powers of PS), sometimes not only Graph SDK is to blame.
I think the source of those problems is the Azure AD Graph… which is now being deprecated, but whose code and metadata advanced into the Microsoft Graph and is picked up by AutoREST when it generates the SDK cmdlets… Decisions taken years ago can have big consequences.
Well said. It’s obvious that in developing the Microsoft PowerShell Graph API Microsoft lost sight of the 80/20 rule – 80% of people only use 20% of the functionality of the software. In the old modules, the 20% that most people probably use (user management, group management, license management etc) worked fairly well, including things like pipeline support. Microsoft now move to Graph, and one of the clear benefits is that you can access nearly 100% of the API using the PowerShell cmdlets. But the problem is – *most people don’t care* – we just want the 20% that we need to work smoothly.
They would do well to focus on that ‘core functionality’ before trying to do more work on the stuff many people will touch rarely, if ever.
“The workaround is to store the set of objects in an array and then process the objects with a ForEach loop.”
That’s like…back to VBS era from 15 years ago ? Sounds insane to me…
In some cases, insanity is the way things work…
This SDK reminds me of Visual Studio Code : a tool made by developers for developers, bloated with tons of useless stuff for admins and regressions (like a Windows 3.11 UI).
Admins need efficient, intuitive, consistent, modular, well-documented scripting tools. A lot of us do not use nor write apps for day-to-day tasks like querying account properties.
Just rewriting PS scripts to remove the pipeline will make them much more complex, long, and hard to maintain. There was a reason Powershell was a welcomed revolution so many years ago !
I really don’t understand how could it come to this. VBScript as a leading technology for Azure scripting in the next ten years at least, really ?
Sorry for the rant. It feels like Azure tooling is going backwards since the Graph stack showed up, which is very frustrating. We could use at least more and better communication from Microsoft about this.