How to Use -Filter with Microsoft Graph PowerShell

The -Filter parameter used in Microsoft Graph PowerShell, enables you to perform a server-side selection (or ‘filter’) of results from your PowerShell client. This means that by using -Filter in your commands, you can get specific results quickly and efficiently. These results can then be reported on, analysed, exported or manipulated based on your needs.

Selecting data using the filter method is an effective and efficient way of achieving your desired results. By only returning your desired result, you are minimising your bandwidth usage, reducing your client-side resource usage and arguably, bettering security.

In this tutorial, I will show you how to use the filter parameter with Microsoft Graph PowerShell and provide practical examples you can use yourself.

-Filter vs Where-Object

In my opening statement, I mentioned that using -Filter is an efficient way of achieving your desired results, this is compared to other sorting methods you may be familiar with, such as Where or Where-Object.

Using the -Filter parameter is generally more efficient than the Where or Where-Object command. This is because although they both can produce the same results, the Where command will first retrieve a complete list of results, and then filter them on the client, whereas the -Filter parameter will send the filter request to the server, the query will then be processed and it will only return the filtered results to the client (you). 

This means that if you have a lot of objects in your directories when using the Where command, they will all be downloaded over the internet to your local device and then they will be processed which takes time and resources. However the -Filter command will only download the filtered results to your local device which makes it faster and more efficient.

Although small in comparison, below you can see the time taken to complete the same command using both the Where command and -Filter parameter is drastically different:

Where Graph Results
Where Graph Results
Filter Graph Results
Filter Graph Results

When to use the ConsistencyLevel and CountVariable parameters

Occasionally you may come across the ConsistencyLevel and CountVariable parameters added to commands to scripts. These are needed when making more advanced queries to Microsoft Graph. Below I have explained what each parameter does.

-ConsistencyLevel

Microsoft Graph indexes exponentially large amounts of data across the globe. Naturally, this data is stored across multiple services on many servers. When this data is updated, the updated data has to copy across all the necessary systems and servers, if this didn’t happen, or if you tried to look up data before this copy was completed, your results may not be consistent. To ensure consistency when using search features with advanced capabilities, the Eventual consistency level needs to be manually defined with the -ConsistencyLevel parameter.

-CountVariable

The count variable is usually required when defining eventual consistency within your command. However, its place and requirements are not that clear… Although retrieving a count of results is sometimes helpful, running your command using eventual consistency, without the -CountVariable parameter will cause the command to fail with an unsupported filter operator or query error. So even if you do not need the count information, you need to define a count variable in your command.

The ConsistencyLevel and CountVariable parameters explained

Both parameters are needed when performing what are called ‘Advanced queries’. Examples of Advanced queries include when using the not (not), ne (not equals) and EndsWith (ends with) operators in your query. 

Unfortunately, not all directory objects support the use of advanced queries with Microsoft Graph. So the way these advanced queries are handled is that when a request is made, internal rules inside Microsoft Graph will determine if it is an advanced query or not. If it is determined the query is an advanced query, the request is sent to a separate service called the ‘Advanced Query Service’ which then retrieves the data from an independent ‘Index Store’ where only the supported data is held. This is different from the Directory store from which a simple query retrieves its data. It is also true that for supported objects, you can run a simple query with the ConsistencyLevel and CountVariable parameters and have that request processed by the advanced query service.

As the index store is independent of the directory store, a one-way sync is performed which will sync only the supported attributes from the directory store to the index store, this explains why the eventual consistency level is necessary. As the Index store is not truly in sync with the Directory store, the eventual consistency ensures that any new directory writes that are made are also available for read jobs by the Advanced query service.

Graph Advanced Queries Flow

Let’s take a look at this change in action. Below I will update the DisplayName attribute for the user “[email protected]”. 

For this test also, we are going to specifically direct a basic query (a query that isn’t defined by the advanced rule in Microsoft Graph) to the Advanced Query Service. We do this by specifying the ConsistencyLevel and CountVariable parameters on a simple request.

Update-MgUser -userid [email protected] -DisplayName "DisplayName Changed"

I will then use the Get-MgUser command to see if this has been updated. One query will be basic and sent to the Rest Directory Service and the other will be sent to the Advanced Queries Service.

#Sent to Rest Directory Service
Get-MgUser -userid [email protected]

#Sent to Advanced Queries Service
Get-MgUser -Filter "UserPrincipalName eq '[email protected]'" -CountVariable CountVar -ConsistencyLevel Eventual

From the outputs below you can see that we are immediately returned accurate information from the Rest Directory Service, but the Advanced Query Service is still showing the old value for the DisplayName.

Example of Advanced Query Service PreSync
Example of Advanced Query Service PreSync

If we just wait a little longer and run our command against the Advanced Query Service again, you can see we are now returned accurate results.

Example of Advanced Query Service PostSync

Unfortunately, this test also demonstrates a lack of validation while using the Advanced Query Service for GET requests. So when in doubt, you can sanity check your results with a simple query (which may return a lot more data) or the web portal.

Using single operators with examples

I often think of each filter query as a rule and a property value must match your rule to be returned as an output on your screen. In its simplest form, a rule can be defined in your filter query using a single operator, for example; this property equals that, or this property is more than this. Lets have a look at all the possible single operators that can be used in your query.

eq (Equal to)

The equals operator is used to query objects that have an exact match to a specific property. For example, the below command will query for any users with a display name that is exactly equal to what I have defined, which of course will only return a single result.

Get-MgUser -Filter "UserPrincipalName eq '[email protected]'"

ne (Not Equal to)

The not equal to operator is used to query objects that do not match exactly the specific property that has been defined. For example, the below command will query for any user which does not match what I have defined, which will return many results.

Get-MgUser -Filter "UserPrincipalName ne '[email protected]'" -CountVariable CountVar -ConsistencyLevel eventual 

not and eq (Not and equal to)

The not and equal to operator achieves the same result as not equal to, by querying only objects where a specific property does not match what has been defined. For example, the below command will query for any user who does not match the username that has been defined.

Get-MgUser -Filter "NOT(UserPrincipalName eq '[email protected]')" -CountVariable CountVar -ConsistencyLevel eventual 

in (equal to a value in a collection)

The in operator will search for objects where the property value matches any values that have been defined in the collection. For example, the below query will search for any objects where the department property matches ‘R&D’ or ‘IT’.

Get-MgUser -Filter "Department in ('R&D', 'IT')"

not and in (Not equal to a value in a collection)

The not and in operator performs the opposite query to the in operator. It will query any objects where the property does not match what has been defined in the collection. For example, the below query will filter for objects where the department property does not match ‘R&D’ or ‘IT’.

Get-MgUser -Filter "NOT(Department in ('R&D', 'IT'))" -CountVariable CountVar -ConsistencyLevel eventual 

le (Less than or equal to)

The less than or equal to operator will filter for any objects where the property value is less than or equal to what has been specified in the query. For example, the below query will filter for any users where the last sign-in date is less than or equal to 23-01/01 12:00:00. Another variations of this operator include lt (less than) which will filter of object that are less than the defined value.

Get-MgUser -Filter "SigninActivity/LastSignInDateTime le 2023-01-01T12:00:00Z"

ge (Greater than or equal to)

The greater than or equal to operator will filter for any objects where the property value is greater than or equal to what has been specified in the query. For example, the below query will filter for any users where the last sign-in date is greater than or equal to 23-01/01 12:00:00. Another variation of this operator is gt (greater than) which will filter of object that are greater than the defined value.

Get-MgUser -Filter "SigninActivity/LastSignInDateTime ge 2023-01-01T12:00:00Z"

startsWith (value starts with)

The startsWith operator will filter for any objects where the property value starts with a particular string value. For example, the below query will return any objects where the Mail property starts with the word ‘Dan’.

Get-MgUser -Filter "startsWith(Mail, 'D')"

not and startsWith (value does not start with)

The not and startsWith operator will filter for any objects where the property value does not start with a particular string value. For example, the below query will return any objects where the Mail property does not start with the word ‘Dan’.

Get-MgUser -Filter "not(startsWith(Mail, 'D'))" -ConsistencyLevel eventual -CountVariable CountVar -All

endsWith (Value ends with)

The endsWith operator will filter for any objects where the property value ends with a particular string value. For example, the below query will return any objects where the User Principal Name property ends with the value ‘onmicrosoft.com’.

Get-MgUser -Filter "endsWith(UserPrincipalName, 'onmicrosoft.com')" -ConsistencyLevel eventual -CountVariable CountVar -All

not and endsWith (Value does not end with)

The not and endsWith operator will filter for any objects where the property value does not end with a particular string value. For example, the below query will return any objects where the User Principal Name property does no end with the value ‘onmicrosoft.com’.

Get-MgUser -Filter "not(endsWith(UserPrincipalName, 'onmicrosoft.com'))" -ConsistencyLevel eventual -CountVariable CountVar -All

contains (Value contains)

The contains operator will filter for any objects where the property value contains a specific string value within in, whether that be at the start, end or within the middle of the property value. For example, the below filter will return any objects where the device name as the work Home in it.

Get-MgDeviceManagementManagedDevice -Filter "contains(deviceName, 'Home')" 

has (Property has value within)

The has operator will return any objects that have a specific value within a collection of values on a specific property collection. For example, the below filter will return any conditional access policy template objects that have the string ‘ZeroTrust’ within the scenarios property collection.

Get-MgIdentityConditionalAccessTemplate -Filter "scenarios has 'ZeroTrust'"

How to use multiple operators in a single query

Using multiple operators in a single query is useful when you really want to drill down and fund very specific information from your directory, it is also very easy to do. 

For example, below I have filtered for users where their mail address ends with ‘it-career.online’ or ‘ourcloudnetwork.com, this query makes use of the OR operator. 

Also below I have used the (back tick) to wrap my code so it is easier for you to read. It can still be copied and pasted as it is.

Get-MgUser -Filter `
"endsWith(mail,'it-career.online') OR endsWith(mail,'ourcloudnetwork.com')" `
-CountVariable CountVar -ConsistencyLevel eventual

The AND operator can also be used to join rules within a single query. For example, I can use the AND operator to filter for users who have the fallback domain in their mail address as well as the job title of Director.

Get-MgUser -Filter `
"endsWith(mail,'microsoft.com') AND jobtitle eq 'Director'" `
-CountVariable CountVar -ConsistencyLevel eventual

Filter a collection of primitive types (Lambda operators)

Lambda operators or Lambda expressions are used to separate the Lambdas parameter list from its body. Basically, on the left-hand side of the Operator (any/all) you have the property, and on the right side, the statement. 

Let’s look at what a Lambda operator query looks like:

(The below query will filter for any user who has the license SKU ‘184efa21-98c3-4e5d-95ab-d07053a96e67’ assigned to them)

assignedLicenses/any(x:x/skuId eq 184efa21-98c3-4e5d-95ab-d07053a96e67)”

  • assignedLicenses = This is the initial target property, this property will contain a collection of objects or values.
  • any = This is the Lambda operator. Any means to match any of the values with the value defined will return as true.
  • x:x = This is used as a range variable, which will hold the current item during iteration while the filter processes through a looping mechanism. This is because the Lambda operator is designed to search a collection of items within a property (an array, so to speak). The character uses each side of the colon ‘:’ are not relevant, you can literally use anything, such as p:p, sku:sku or L:L for example.
  • skuId = This is a property of one of the items in the initial property array. You can imagine, that because most used in Microsoft 365 may have more than one product license assigned, the assignedLicenses property will naturally be an array of items.
  • eq = This is the logical operator for the matching query. This states that the filter will find any item in the property where the SkuId property of that item equals the defined value.
  • 184efa21-98c3-4e5d-95ab-d07053a96e67 = This is the defined value for the SkuId. This is the value that will be analysed against the values of the filter property, which in this case, is the SkuId.
Get-MgUser -Filter "assignedLicenses/any(s:s/skuId eq 184efa21-98c3-4e5d-95ab-d07053a96e67)"

Let’s look at another example, here I want to filter for any user that has the Azure Active Directory Premium Service (AADPremiumService) plan assigned to them:

I am first going to find the relevant property I want to target, in this case, I am going to look at the AssignedPlans Property for a specific user and expand the property to view in the console.

Get-MgUser -userid [email protected] -Property * | `
Select AssignedPlans -ExpandProperty AssignedPlans

Here is the output:

AADPremiumService

You can see that in the above image, the AssignedPlans property contains a collection of items relating to each service plan assigned to this user. And each item in the AssignedPlans property contains 4 of its own properties; AssignedDateTime, CompatibilityStatus, Service & ServicePlanId. I have highlighted above that the information we need is in the Service column, so that is what we are going to use to create our filter query.

Below is the filter query which will filter by the information highlighted above:

Get-MgUser -Filter "AssignedPlans/any(x:x/Service eq 'exchange')" `
-CountVariable CountVar -ConsistencyLevel eventual

Filter results using Invoke-MgGraphRequest

Not all information can necessarily be found with their specific PowerShell cmdlets. This is because not every API path has had a cmdlet developed to utilise the API capabilities. Luckily for us, the authentication module contains the Invoke-MgGraphRequest command which is very Powerfull and fundamentally is what many of the cmdlets are built using behind the scenes. If you want to know more about this cmdlet, check out my tutorial: How To Use Invoke-MgGraphRequest with PowerShell.

Thanks to the nature of this command, any of the operators and filter queries I have used above in this tutorial can also be used with the Invoke-MgGraphRequest cmdlet. It works by defining the filter query at the end of the URI path where an item is found within the Microsoft Graph rest directory or advanced query service. The queries are identical to what I have defined above and are defined at the end of the URI, prepended with $filter. Notice from the below example, I have added a (backtick) to the URI. This is so $filter is not recognised as a variable within PowerShell, otherwise no filtering will apply.

For example, this filter will find any item, where the allowedCombinations property collection contains the string ‘SMS.

$filter=allowedCombinations/any(t:t+has+'SMS')"

As this property relates to Authentication Strength policies, I am going to append this filter to the end of the URI path that indexes the Authentication Strength policies:

$uri = "https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies?`$filter=allowedCombinations/any(t:t+has+'SMS')"

$result = Invoke-MgGraphRequest -uri $uri -Method GET -OutputType PSObject

$result.value

Below you can see the expected output from the above commands:

Invoke Mg Graph Results
Invoke Mg Graph Results

Daniel Bradley

My name is Daniel Bradley and I work with Microsoft 365 and Azure as an Engineer and Consultant. I enjoy writing technical content for you and engaging with the community. All opinions are my own.

This Post Has One Comment

  1. Dinesh

    Thank you for sharing this great article. Your contribution is appreciated.

Leave a Reply