notcontains not working for User objects pulled from AD Groups

tl;dr

Don’t store the AD user objects themselves in your arrays, use their .SamAccountName property value instead (generally, pick a property that uniquely identifies the objects):

# ...
$SPSecUKUsers += $SPSecUser.SamAccountName
# ...
if ($SPSecUKUsers -notcontains $UKUser.SamAccountName) { # ...

See the next section, if you want to know why storing the objects themselves doesn’t work.

Alternatively – for faster lookups – use a hashtable:

$SPSecUKUsers = @{} # initialize hashtabe
# ...
# Create an entry for the object at hand, using its .SamAccountName
# as the entry *key*; you can store the object itself as the entry *value*.
# If all you need are lookups by SAM account name, however, you can just
# use a fixed value such as $true.
$SPSecUKUsers[$SPSecUser.SamAccountName] = $SPSecUser
# ...
if ($SPSecUKUsers.ContainsKey($UKUser.SamAccountName)) { # ...

About PowerShell’s containment (collection-membership) operators:

As Olaf and Lee_Dailey imply in the comments, PowerShell’s containment operators (-contains / notcontains and -in / -notin) check the comparison operand for reference equality (identity) with the elements of the input array, if those elements are instance of .NET reference types, with the exception of [string] instances. [string] instances and instance of value types are tested with
value equality (equivalence) – see Equality Comparisons.

You can think of these operators as an implicit loop over the input array’s elements, testing each against the comparison operand with the -eq operator (or, if you use the case-sensitive variant such as -ccontains, with -ceq), using reference equality or value equality, depending on the element type.

Important: Due to PowerShell’s flexible automatic type-conversion rules, which operand is the LHS in an -eq operation matters. Using -in or -contains means that the LHS of the implied -eq operation is the array element being tested against, as the following examples show:

 # `, 10` creates a single-element array
 '0xa' -in , 10       # equivalent of: 10 -eq '0xa' => $true
 , 10 -contains '0xa' # ditto

 # 
 10 -in , '0xa'        # equivalent of: '0xa' -eq 10 => $false
 , '0xa' -contains 10  # ditto

In the first 2 operations, the LHS being a number ([int]) forces the string RHS ([string]) to a number ([int]) too, and the hex “number string” '0xa' converts to an [int] with decimal value 10 too.

In the latter 2, the LHS being a string ([string]) forces the number 10 to become a string too, and '10' obviously doesn’t match '0xa'.

Value equality (equivalence) means that two objects have the same content, even though, with distinct value-type objects, that content is by definition stored in different memory locations.

Numeric types such as [int] and [double] are value types, for instance. As a rough rule of thumb, objects that have properties are often reference types.
You can check a given type’s .IsValueType property; e.g., [int].IsValueType returns $true.

Reference equality (identity) means that two values are only considered equal if they point to the very same object in memory, i.e., the same instance of a reference type.

Otherwise, they’re considered not equal, even if they represent what is conceptually the same entity, which is what happened in your case: two separate calls to Get-ADUser return distinct objects, even if you (in part) ask for the same users in both cases (Get-ADUser returns instance of type Microsoft.ActiveDirectory.Management.ADUser, which is a reference type).

Examples:

# Create a custom object...
$customObject = [pscustomobject] @{ one = 1; two = 2 }
# which is an instance of a reference type.
$customObject.GetType().IsValueType # -> $false

# Create an array comprising a value-type instance (1)
# and a reference-type instance (the custom object).
$arr = 1, $customObject 

# Look for the value-type instance.
$objectToLookFor = 1

$arr -contains $objectToLookFor # value equality -> $true

# Create another custom object, with the same properties as above.
$objectToLookFor = [pscustomobject] @{ one = 1; two = 2 }

# This lookup *fails*, because $objectToLookFor, despite having the same
# properties as the custom object stored in the array, is a *different object* 
$arr -contains $objectToLookFor # reference equality -> $false(!)

# If we look for the very same object stored in the array, the lookup
# succeeds.
$arr -contains $customObject # -> $true

Leave a Comment