PowerShell Heaven - Optimizing your Microsoft Graph Usage
First off, I am a great fan of PowerShell. It is a very versatile and powerful scripting language that, ever since the ‘.NET Core revolution’, works cross-platform as well. Awesome!
While the language in itself is cool, its installable extensions called PowerShell Modules, usually delivering ‘one-liner’ cmd-lets that are tailor-made to configure a certain platform or application, are what makes PowerShell truly Powerful!
The wonders of the Invoke-PnPGraphMethod
cmd-let!
A Plethora of Modules
While both the PowerShell ‘core language’ and its extensibility in the form of Modules are absolutely great I did notice an explosive rise in the number of Microsoft-built ‘Modules’ available. Each covering a seemingly different segment of the Microsoft Cloud Services.
This represents somewhat of a double-edged sword.
On the one hand PowerShell and its Modules beat configuring stuff through a GUI any day of the week.
However, Modules can sometimes be hard to unify, they might have overlapping capabilities, have independent life-cycle and ‘major update’ (read: ‘breaking changes’) schedules and sometimes have massive disk space requirements (like the AZ PowerShell Modules). So, using more modules in a solution, will make such a solution harder to manage.
Compared to the ‘On Premises’ era, where you would probably use Active Directory, Exchange, SQL and of course the famous (now legacy) SharePointPnPPowerShellOnline (not to be mistaken with my favorite Module still, the PnP.PowerShell Module) Modules, to manage a large part of the Microsoft ecosystem, we now have a Cloud reality and the number of Modules skyrocketed.
As a side-note, most of the MS provided modules can be found here.
Some very obscure ones like the MSCommerce Module (very important to limit Self-Service licensing capabilities in your tenant) are not even mentioned BTW, so expect the list to be larger.
PowerShell Modules: Always at least one step behind
One important thing to realize is that PowerShell Modules’ cmd-lets always represent an ‘abstraction’ of the capabilities of the underlying API they expose. As such these modules are at least ‘one step behind’ in delivering the said API’s capabilities.
Following this, another thing to realize is that even though the authors of the Module will always try to stay as close to an API’s intended purpose as possible, choices (as in: ‘design decisions’) have to be made at some point and ‘bias’1 is introduced.
PowerShell Modules: Latest updates and trends
When it comes to specifying one of the biggest ‘trends’ concerning PowerShell Modules used for manipulating the Microsoft Cloud it would be ‘The Slow but Certain Move to the Microsoft Graph’.
Some important Microsoft 365 management Modules like ‘Azure AD’ and ‘MSOnline’ are now scheduled for deprecation, they are slowly being replaced with MS Graph enabled alternatives, like ‘the king’ of Graph-enabled modules: the Microsoft Graph PowerShell SDK. Sadly not everyone at Microsoft have gotten the message, as new capabilities and documentation references to the mentioned ‘soon to be legacy’ Modules are still being added to this day.
A very noteworthy Module, the PnP.PowerShell, is also slowly transitioning to use the MS Graph under the hood.
We can see the ‘ExchangeOnline’ Module recently got an update to ‘v3’ where modern sign-in/connection methodologies were implemented and, by removing the WinRM dependency, this module has now gained reliable and supported cross-platform capabilities.
These are just a few of recent changes and updates within the PowerShell Module ecosystem and you really have to be ‘on top’ of all of these changes to make sure the Modules you use ‘play nice’ together and update your scripts when a breaking change2 happens.
The MS Graph is here to stay
I want to express my hope that more and more Modules covering the Microsoft cloud ecosystem will be transitioned to the Microsoft Graph and that we have less Modules to manage (going against the trend, I know) in a few years from now.
It is clear as day IMHO that the Microsoft Graph is a keeper and will be with us for a long time. So while most actively developed Modules (Microsoft-provided, third-party and community Modules alike) are making the transition, this got me thinking.
Why not stay as close to the source (ie. the Microsoft Graph API) as you can get while developing PowerShell solutions?
My reasoning:
- The Microsoft Graph documentation is great (also for the beta endpoint) and you can really count on it to be accurate. The examples provided are also excellent.
- Installing numerous Modules (especially in pipeline operations, where Modules need to be installed on each run of the pipeline) can be quite a lengthy process, adding to the overhead of total script execution.
- Being dependent on Module maintainers to release a certain parameter for a cmd-let to accommodate fresh new additional functionality for the Graph API, is undesirable.
- As an IT Pro you might broaden your professional reach, closing the gap to development paradigms and solutions by learning and using more of the underlying API.
- And finally, the most important reason I can think of: Build better scripts, using your own business-driven ‘bias’.
So while the Microsoft Graph PowerShell SDK is available and the peeps of the PnP.PowerShell initiative are busy making the MS Graph transition, all we really need is a steady/stable authentication process to connect to the Microsoft Graph and query the API (GET, PUT, DELETE, etc.) directly.
A great Module is available, which I recommended in a previous blog post, called EasyGraph.
BUT… wait for it. The aforementioned and acclaimed PnP.PowerShell Module developers have added that same ‘query the MS Graph directly’ functionality in their Module. Now we can benefit from:
- the vast array of cmd-lets already present in this Module;
- all the different authentication methods that this Module provides;
- having the option to query the Graph directly.
FANTASTIC!
Example: Using the MS Graph through PowerShell
So, the best reason to query the Graph directly is simply to write better scripts that perform better for your specific business scenario. I will be explaining that now using both the ‘use existing cmd-let’ and ‘query the Graph directly’ options, making use of the PnP.PowerShell Module.
Imagine we had the following requirement:
Deliver a Report:
listing all Teams
with their Owner(s)
Option 1 - Built-in Module cmd-let
Now, you could use the built-in cmd-let Get-PnPMicrosoft365Group
with the -IncludeOwners
parameter. Which is probably fine by itself.
1
2
3
4
# Fetch all Teams including Owners
$Result = Get-PnPMicrosoft365Group -IncludeOwners
# Show results of the first returned Team
$Result[0] | Select-Object -Property @(@{L = "TeamDisplayName"; E = {$_.DisplayName}}, @{L = "TeamMailNickname"; E = {$_.MailNickname}}) -ExpandProperty Owners | Select-Object TeamDisplayName,TeamMailNickname,Email
The first command in this snippet executes a batched operation (Click the link to understand the ‘bias’ that will always be inherent for an abstraction like this.3) to fetch the Owners of each Team (with pretty good performance, I might add).
However we could make a direct call that performs even better by using just one API call (no batching of calls required) and return only the properties we need, not more. Because if you would run…:
1
$Result[0] | Select-Object *
… it is clear that a lot more (some being irrelevant to our need) properties are returned. The cmd-let is actually over-delivering here.
Option 2 - Direct Query
Now we will devise a single call that returns only the information of the Teams we require. No more, no less.
In comes the power of ‘ODATA query options’.
ODATA is a protocol employed by the MS Graph to allow for, among other things, ‘structured querying’, very much like querying a database.
For example, we could use the $select
, $filter
and/or $expand
operations to structure an API Query URL in such a way that it will deliver exactly the right data for our usage.
1
2
3
4
5
6
# Get endpoint
$Uri = "v1.0/groups/?`$select=id,displayName,mailNickname&`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$expand=owners(`$select=mail)"
# Place the call
$Result = Invoke-PnPGraphMethod -Method Get -Url "$($Uri)" -ContentType "application/json" -All -ErrorAction Stop
# Show results of the first returned Team
$Result[0].value | Select-Object -Property @(@{L = "TeamDisplayName"; E = {$_.displayName}}, @{L = "TeamMailNickname"; E = {$_.mailNickname}}) -ExpandProperty owners | Select-Object TeamDisplayName,TeamMailNickname,mail
The cmd-let Invoke-PnPGraphMethod
will touch the groups
endpoint through a GET request method, limit the properties returned through the $select
operation and $expand
the owner information for each Team.
The specific documentation for this endpoint will show you some examples and the Graph permissions required to make the call. This is important if you want to adhere to a ‘least privilege’ operational stance. Notice the clarity and repeated structure of this documentation for all endpoints.
While the endpoint $Uri
construct might scare some people, I tend to prefer this over the ‘one size, fits all’ approach of the default Module cmd-lets.
Now the only returned properties will be the ones that are explicitly requested (not possible with Option 1 at the moment) as you will see after running:
1
$Result[0].value | Select-Object *
Probably, (especially if you have many Teams in your tenant) the query presented by Option 2 will return a bit quicker compared to Option 1.
No extra calls (except for the obvious ‘paging’ of the results, which is also present in Option 1) will be executed, making this somewhat more performant4 than Option 1.
Also, you have learned more about the nature of the Microsoft Graph and the nature of PowerShell Module ‘API abstractions’ which is always a good thing!
-
a bias is an inclination toward something, or a predisposition, partiality, prejudice, preference, or predilection. ↩
-
Microsoft removed the operational status for some ExchangeOnline cmd-lets when used in an App Only context. Learn more. ↩
-
In this case the ‘bias’ manifests itself by a preference to use a ‘batching strategy’ in favor of using the ‘built-in ODATA query option’
$expand
to fetch the Team’s Owner information. This is a logical preference for the Module’s implementation of the MS Graph API because batching scales better if multiple properties need to be expanded. In our example we only need one property, so the use of batching would be overkill. ↩ -
The word ‘performant’ in this post means something along the lines of ‘speedy (both the number of API calls and used bandwidth are taken into consideration), accurate, flexible and capable’. ↩