PowerScript Filtering Paradox

98 Views Asked by At

Some records have account type N and others have account type G in this example.

Why does the filter work in this script:

Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties name, uht-IdentityManagement-AccountType} | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")}

i.e., you get only the G records, but not this one:

Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties * | select name, uht-IdentityManagement-AccountType} | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")}

i.e., you get both the N and G records.

The extra "pipe" in the second script allows for a much nicer format since you get much less data per record; you get columnar output, one row per record.

I have tried -notlike "N" to no avail.

2

There are 2 best solutions below

0
On

The issue is not with the properties or filtering but with the position of the Select statement, and how it changes the following statements in the pipeline.

The first statement:

Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties name, uht-IdentityManagement-AccountType} | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")}

Can be broken up into 2 statements:

$AllUsers = $Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties name, uht-IdentityManagement-AccountType}

$AllUsers | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")}

Similarly, the second statement:

Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties * | select name, uht-IdentityManagement-AccountType} | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")}

Can be broken up into 2 statements:

$AllUsers2 = Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties * | select name, uht-IdentityManagement-AccountType} 

$AllUsers2 | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")}

Both statements generate arrays:

PS C:\> $AllUsers.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

PS C:\> $AllUsers2.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

But the objects inside the array are different.

PS C:\> $AllUsers[0].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ADUser                                   Microsoft.ActiveDirectory.Management.ADAccount

PS C:\> $AllUsers2[0].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ADUser                                   System.Object

The first is an array of Type ADAccount and the second is an array of Sytem.Object accounts. This is ok if you are iterating over (and filtering over) the items inside the array. But this is different when you are iterating/filtering over properties inside the array.

The way you use Where-Object tries to invoke the GetEnumerator() Method, which the Type ADAccount conveniently has, but System.Object does not.

PS C:\> $AllUsers | Get-Member

   TypeName: Microsoft.ActiveDirectory.Management.ADUser

Name                   MemberType            Definition
----                   ----------            ----------
GetEnumerator          Method                System.Collections.IDictionaryEnumerator GetEnumerator()
Name                   Property              System.String Name {get;}
...

PS C:\> $AllUsers2 | Get-Member

   TypeName: Selected.Microsoft.ActiveDirectory.Management.ADUser

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
Name        NoteProperty string name=HAL9256

Therefore it can't enumerate over the properties and properly perform the Where-Object filtering.

The better method is to do the "formatting" of Select at the end of the pipeline:

Get-Content "MembersMS.txt" | ForEach-Object {Get-ADUser -Identity $_ -Properties name, uht-IdentityManagement-AccountType} | Where-Object {($_['uht-IdentityManagement-AccountType'] -ne "N")} } | Select name, uht-IdentityManagement-AccountType
0
On

To explain why this happens we need to look at ADPropertyCollection Class, this class, similar to PropertyValueCollection Class, have a Item[String] Property which, basically allows getting a property value by index, i.e.:

(Get-ADUser krbtgt)['samAccountName']

Would work fine to get the samAccountName of krbtgt.

However, if we attempt to use Select-Object to filter some of the object's properties, the object would no longer be of the same type (ADUser in this case), it would be a PSObject and PSObject's don't have an Item[String] parameterized property:

# no longer works
(Get-ADUser krbtgt | Select-Object samAccountName)['samAccountName']

So ideally, you should use Select-Object as the last statement of your pipeline. Do note, Get-ADUser already supports piping identities so ForEach-Object is not needed:

Get-Content "MembersMS.txt" | Get-ADUser -Properties uht-IdentityManagement-AccountType |
    Where-Object { $_['uht-IdentityManagement-AccountType'] -ne "N" } |
    Select-Object name, uht-IdentityManagement-AccountType

It's also worth noting that, if you where using dot notation instead of indexing, both code snippets would've worked:

Where-Object { $_.'uht-IdentityManagement-AccountType' -ne "N" }