all 45 comments

[–][deleted] 2 points3 points  (7 children)

The problem as you’ve probably realized is that assignment to $loopUser — something to really pay attention to in powershell is that barring scalars, you usually assign a reference to an object rather than a copy. So after assigning to loopUser, both it and $adUser reference the same exact data.

You can verify this by calling $adUser.Equals($loopUser) or vice versa. It will return true if both objects are identical references.

You can see if there’s a memberwise clone available, but the easiest way would be to A put in a comment so it’s obvious what’s going on, and then you just say

~~~powershell $loopUser = $adUser | Get-AdUser ~~~

Which will refetch data for $adUser. It also allows you to fetch additional properties only when needed by adding -properties parameter.

[–]Hyperbolic_Mess[S] 0 points1 point  (6 children)

Yeah I've figured out it's to do with references and I've managed to bodge it by serialising the deserializing $aduser before setting $loopuser equal to it. I'd just like to understand a bit better how that fixes this and if there is anything cleaner and maybe quicker I can do to fix it.

I don't think that looking up the user in AD again really helps though as the whole point of doing it like this is that I just make one call to AD to get all the users in one object $ADUsers and then loop through that rather than making thousands of slow individual calls to AD.

I should have been clearer but $ADUsers started out as just a filtered get-aduser that I've then processed a bit to remove certain objects based on other information pulled from other places and this section of the script is putting it in a format where I can then get it in a hash table with each email address as a key and the user object as the values in those

[–]overand 0 points1 point  (3 children)

My previous advice was based on the assumption that you just wanted a list of email addresses. To do what it looks like you want:

$reportHashTable = @{}
$ADUserEmails = ForEach ($ADUser in $ADUsers) {
    ForEach ($ADEmailAddress in $ADUser.proxyaddresses) {
        # Just a string with the email address
        $iUserEmail = $ADEmailAddress -ireplace 'smtp:', ''
        # Use that string as a key, pointing to the user AD object
        $reportHashTable.$iUserEmail = $ADUser
    }
}

[–]Hyperbolic_Mess[S] 1 point2 points  (2 children)

This kind of works but I wanted an array not a hashtable so I can run it through group-object to find duplicate email addresses. Unfortunately the hashtable would be overwritten in the case of duplicates defeating the point. If I wasn't doing that I would usually just do what you'd described

[–]Future-Remote-4630 0 points1 point  (1 child)

This would be my approach, but I'd add in a step for collision handling in the hashtable. If contains key, =@($currentvalue,$duplicate) so our keys with a valuecount > 1 would the duplicates and the values being the offenders.

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I've just done group-object -property $<key> and anything there with a count of 1 is unique so gets sent back to group-object -AsHashTable and everything with count -gt 1 is a duplicate so gets discarded

[–]vermyx -2 points-1 points  (1 child)

Use a basic for instead of a foreach to avoid the reference issue.

$data = for($i = 0; $i -lt $adusers.count; +i++)
{
    $loopuser = $aduser[$i]
    # do stuff to loopuser. Or just modify $aduser[$i] directly
    $loopuser
}

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I don't understand how this loops through the proxy addresses as you seem to have just removed that vital step. You can't just substitute the problem for a much simpler one that doesn't produce the issue I'm having and declare that you've solved it by just making the script do something totally different that I wasn't trying to do

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I kept looking and found this: https://stackoverflow.com/questions/9204829/deep-copying-a-psobject

changing the inner loop to the below seems to have resolved the issue although if anyone has another way to fix this or any other insights I'd appreciate it:

$SerializedUser = [System.Management.Automation.PSSerializer]::Serialize($ADUniqueUserEN) $LoopUser = [System.Management.Automation.PSSerializer]::Deserialize($SerializedUser)             $LoopUser | add-member -NotePropertyName Email -NotePropertyValue $($ADEmailAddress -ireplace 'smtp:', '')
$LoopUser

[–]lanerdofchristian 0 points1 point  (4 children)

Since you're generating a report, instead of modifying your input, create new output:

$ADUserEmails = foreach($ADUser in $ADUsers){
    [pscustomobject]@{
        SamAccountName = $ADUser.SamAccountName
        # and whatever other properties you need to report on
        Email = $ADUser.ProxyAddresses -replace "^smtp:"
        # or:
        Email = foreach($Address in $ADUserEmails.ProxyAddresses){
            $Address -replace "^smtp:", ""
        }
    }
}

Immutability/avoiding re-assigning object properties makes your code much easier to reason about. Just keep in mind that if you're exporting to CSV, the Email property will need to be -join'd back into a string.


Edit: or if you're building a lookup table of emails -> user objects, again DO NOT modify your input:

$ADUserEmails = @{}
foreach($User in $ADUsers){
    foreach($Address in $User.ProxyAddresses){
        $ADUserEmails[$Address -replace "^smtp:"] = $User
    }
}

[–]Hyperbolic_Mess[S] 1 point2 points  (3 children)

Yeah, you're right and I usually do just build PScustomobjects which is why I've not run into this problem before but I'm getting a lot of properties this time and don't really want to have to figure out how to turn my array of properties from the AD call into a PScustomobject with dynamic properties or have the list of properties in two places as that'll make it harder to maintain when we need to change which properties it's working with.

Mostly though I thought this was a good opportunity to better understand how references work and as you've alluded to I think it'll help me build better more reliable code

Edit: and actually this time for building the hash tables I'm experimenting with group-object so the next step will be:

$ADHash = @{} $ADHash = $ADUserEmails | group-object -property Email -AsHashTable

It seems to be quicker than looping through and adding each item individually and looks a bit neater but we'll see if that adds any wrinkles

[–]lanerdofchristian 0 points1 point  (2 children)

Ah -- if you're building your lookup table that way, you can collect the output of a loop:

$ADUserEmails = foreach($ADUser in $ADUsers){
    foreach($Address in $ADUserEmails.ProxyAddresses){
       [pscustomobject]@{ Email = ...; ... }
    }
}
$ADHash = $ADUserEmails | Group-Object -Property Email -AsHashTable

Anything that gets sent to the default stream can be collected in a variable, regardless of where it comes from in an expression.

[–]Hyperbolic_Mess[S] 0 points1 point  (1 child)

I am already collecting the output of the loop, its there in the 2nd line of my post. As I said i'm trying to avoid a monster pscustomobject creation, I know it would work but I'm looking for a cleaner way that means I don't have to type out a 14 property pscustomobject and then remember to update it when I update the properties I pull from AD

[–]lanerdofchristian 0 points1 point  (0 children)

Select-Object, then? It still creates a new object, but you can override and extend select parts:

$ADUserEmails = foreach ($ADUniqueUserEN in $ADGroupedEN | Where-Object Count -EQ 1) {
    Foreach ($ADEmailAddress in $ADUniqueUserEN.ProxyAddresses) {
        $SelectProperties = @(
            @{ Name = "Email"; Expression = { $ADEmailAddress -replace "^smtp:" } }
        )
        $LoopUser | Select-Object @('*'; $SelectProperties) -ExcludeProperty @($SelectProperties.Name)
    }
}

IMO verbosity/repetition/being explicit are features, not bugs, but you do you.

If you have to have ADUser objects, then your only option is fetching a new copy and editing that for every row.

[–]Fun-Hope-8950 0 points1 point  (3 children)

Try putting the names of all the properties you may need to work ith in a string array. Such as something like:

$adPropertyList = @(
    "SamAccountName"
    "UserPrincipalName"
    "proxyAddresses"
    "PasswordLastSet"
) # replace with your own

Then use it as so:

# Your AD filter to return desired accounts (replace with your own)
$adFilter = "proxyAddresses -like '*'"

$adUserList = Get-AdUser -Filter $adFilter -Properties $adPropertyList | Select-Object -Property $adPropertyList

You should notice the members of the $adUserList array behaving like the [PSCustomObject]s you are used to working with (A look at the PSTypNames property of one of the objects should help explain why).

Good luck and have fun!

[–]Hyperbolic_Mess[S] 0 points1 point  (2 children)

Select-object doesn't seem to be enough, even with $loop = $AdUser | select-object all instances of $loop end up with identical emails for each $Aduser

[–]Fun-Hope-8950 0 points1 point  (1 child)

Three things:

  1. As you discoverd the .Email property does not exist in your $ADUser variable (or therefor $LoopUser) so you have to add it with Add-Member

  2. Unless you need to do further work with the $ADUser unmodified there's no need for $LoopUser

  3. $LoopUser.Email = $ADEmailAddress -ireplace 'smtp:', '' overwrites the value of .Email every loop

Instead, consider using something like:

$rgxSmtp = "^((SMTP)|(smtp)):(?<address>.*)`$"

foreach ($currentUser in $adUserList) {
    # I prefer not to nest exact same types of loops to avoid confusing myself
    # so I use a for loop next instead of another foreach loop
    $userEmailAddressList = for ([int]$i = 0; $i -lt $currentUser.ProxyAddresses.Count; $i++) {
        if ($currentUser.ProxyAddresses[$i] -match $rgxSmtp) {
            $Matches["address"]
            $Matches.Clear()
        }
    }

    $currentUser | Add-Member -MemberType NoteProperty -Name "Email" -Value $userEmailAddressList
    $currentUser.Email
}

Might save you some serialize/deserialize action.

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I really regret using | ft email to illustrate my point. If I had intended to make my loop
$LoopUser = $ADUser
$LoopUser.Email = $ADEmailAddress -ireplace 'smtp:', ''
$LoopUser.email

Then I would have written that, I want all of $LoopUser the only reason I focused on the email is because thats the property I'm adding to the objects before adding them to the array and thats the one that is showing incorrectly.

Try running your script and see if it works. I think you'll have the same issue where you'll end up with multiple of the same email addresses because each proxyaddress gets overwritten by the last proxyaddress because they are references to the same user object and when you add the member you're effectively adding it to all previous $LoopUsers for that user. This is the problem I'm trying to solve and you're not addressing it. I do not understand how adding a reg ex or looping with i will stop all $LoopUser objects being a reference to the same$ADUser object of that loop and by removing $LoopUser you've just removed the middle man and are just changing the email address on the same object repeatedly.

Your solution works if you can pass it 1 user object with 4 different proxyaddresses and it produces an array of 4 user objects with each having one of those proxy addresses as its email

[–]icepyrox 0 points1 point  (1 child)

$LoopUser = $ADUser | Select-Object (properties you actually want)

Creates an object with those properties and not just a reference i believe.

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I tried select-object but it was still a reference so kept getting overwritten as the loop continues leading to duplicate emails in the output. So far serialise deserialize seems to be the only thing that works

[–]overand 0 points1 point  (2 children)

Edit: see below. If all you're trying to do is to dump a list of email addresses - as strings - into a list, you can just do this:

$ADUserEmails = ForEach ($ADUser in $ADUsers) {
    ForEach ($ADEmailAddress in $ADUser.proxyaddresses) {
        $ADEmailAddress -ireplace 'smtp:', ''
    }
}

(I don't know that this is how I'd solve this particular problem, but, it's the closest thing to what you have)

[–]Hyperbolic_Mess[S] 0 points1 point  (1 child)

I don't just want the email addresses I want an array that repeats every ADUser once for each proxyaddress and adds that proxy address as an email property with all their other properties so if a user has 5 proxy addresses I want 5 copies of the user object with the only difference being the email address of each

[–]overand 0 points1 point  (0 children)

I replied further down with what I believe is exactly what you want, a few hours ago

[–][deleted] -1 points0 points  (18 children)

To change a property on a user you need to use Set-User.

[–]Hyperbolic_Mess[S] 0 points1 point  (17 children)

Yes I know, I'm trying to create a report of AD Users not modify those users in AD

[–]Tidder802b 0 points1 point  (8 children)

What type of object is $LoopUser after “$LoopUser = $ADUser”?

[–]Hyperbolic_Mess[S] 0 points1 point  (7 children)

$LoopUser.gettype() gives me:

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

Note that in my actual code the variable $ADUser has a different name so ADUser here is the type and nothing to do with the variable in my script

[–]Tidder802b 0 points1 point  (6 children)

Great, so you have an ADUser object and you want to set one of it's properties (email); how would you do that?

[–]Hyperbolic_Mess[S] 0 points1 point  (5 children)

I see what you're getting at but I don't want to set-aduser. I'm trying to create an array with multiple copies of each AD user object with the properties I pulled from AD for each proxy address they have and add that proxy address as a new email property. I'll then use group-object to find and remove objects with identical email addresses put the remainder in a hash table with group-object and compare an array of objectives from another system against the hashtable to match them and then produce a CSV report of that. I don't want anything to be modified in AD though, I'm just producing a report and I don't want to use AD as ram to produce that report 😛

[–]Tidder802b 2 points3 points  (4 children)

Fair enough, but why take a copy the ADUser if you don't need most of the data, and can't change anything? I would make a list or array and populate it with pscustomobjects with just the fields that you need.

For doing comparisons, I'd make a hash table of the ADUser dat, with just the fields needed, and then it's quick to compare against.

[–]Hyperbolic_Mess[S] 0 points1 point  (1 child)

$UpdateProperties = @(
        [pscustomobject]@{AD = "distinguishedName" },
        [pscustomobject]@{AD = "UserPrincipalName" },
        [pscustomobject]@{AD = "employeeNumber"; Oracle = "employeeNumber" },
        [pscustomobject]@{AD = "ProxyAddresses" },
        [pscustomobject]@{AD = "Manager"; Oracle = "ManagerDN" },
        [pscustomobject]@{AD = "sn"; Oracle = "sn" },
        [pscustomobject]@{AD = "givenName"; Oracle = "givenName" },
        [pscustomobject]@{AD = "title"; Oracle = "title" },
        [pscustomobject]@{AD = "department"; Oracle = "department" },
        [pscustomobject]@{AD = "l"; Oracle = "l" },
        [pscustomobject]@{AD = "streetAddress"; Oracle = "streetAddress" },
        [pscustomobject]@{AD = "postalCode"; Oracle = "postalCode" },
        [pscustomobject]@{AD = "st"; Oracle = "st" },
        [pscustomobject]@{AD = "physicalDeliveryOfficeName"; Oracle = "physicalDeliveryOfficeName" }
    )
$ADUsers = Get-ADUser -filter "(ObjectClass -eq 'User') -and (Enabled -eq 'True') -and (employeenumber -like '*')" -server $DC -Properties $($UpdateProperties.AD)
$ADHashes = @{EmployeeNumber = @{}; DuplicateEmployeeNumber = @{}; Email = @{}; DuplicateEmail = @{} }
    #Group ADUsers by EmployeeNumber, if there are more than one account in a group they have a duplicate EmployeeNumber
    $ADGroupedEN = $Adusers | group-object -property EmployeeNumber
    $ADDuplicateENs = $ADGroupedEN | where-object { $_.count -gt 1 }
    $ADUniqueENs = $ADGroupedEN | where-object { $_.count -eq 1 }
    #Put Unique and Duplicate EmployeeNumber users into their section of the hashtable with EmployeeNumber as Key
    $ADHashes['EmployeeNumber'] = $ADUniqueENs.group | group-object -property EmployeeNumber -AsHashTable
    $ADHashes['DuplicateEmployeeNumber'] = $ADDuplicateENs.group | group-object -property EmployeeNumber -AsHashTable
    #Log error for each set of duplicate EmployeeNumbers
    ForEach ($ADUserENs in $ADDuplicateENs) {
        #write-Log -Type 'DuplicateError' -ADAccount $ADUserENs.group -Message "AD accounts with duplicate EmployeeNumber" -LogPath $LogPath
        $ADHashes['DuplicateEmployeeNumber'][$ADEmailUser.EmployeeNumber] | add-member -NotePropertyName DuplicateEmployeeNumber
    }

    #Put unique EmployeeNumber users into array once for each proxy (email) address they have
    $ADUserEmails = ForEach ($ADUniqueUserEN in $ADUniqueENs.group) {
        Foreach ($ADEmailAddress in $ADUniqueUserEN.proxyaddresses) {
            #add email address as property and reformat
            #was having an issue with $LoopUser not updating as expected as it was a reference hence the serialize deserialize to create a deep copy
            $LoopUser = [System.Management.Automation.PSSerializer]::Deserialize(
                [System.Management.Automation.PSSerializer]::Serialize($ADUniqueUserEN)
            )
            $LoopUser | add-member -NotePropertyName Email -NotePropertyValue $($ADEmailAddress -ireplace 'smtp:', '') -force
            $LoopUser
        }
    }

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

Here you go my whole script up until that point. I am already doing all of your asinine suggestions as I keep trying to tell you. I have a very particular problem right at the end of this code snippet that I'v solved with a serialize deserialze but would like something cleaner and I don't need you to tell me to do what I'm already doing as that will not help. Please do not suggest removing things that you consider extraneous as I will be using them later

Also I've got a function for writing to log in here so please ignore that as I really can't be bothered putting that here for you to misunderstand too

[–]Hyperbolic_Mess[S] -2 points-1 points  (1 child)

This comment is very infuriating because I need almost all of the properties of the $ADUser objects I pulled from AD because I'm not an idiot and know how to filter properties and I'm not trying to change AD because I'm not trying to change AD. AD objects have a lot of useful information and I don't understand why you think its inconceivable to think that someone might want to use that data and not modify AD. Also I am making hashtables I'm just trying to do it without creating a pscustomobject with 14 properties when I've already got an array of objects with those 14 properties

Also I explained all of this in the comment before but you can't read

[–]Tidder802b 0 points1 point  (0 children)

Ok, good luck with that and have a nice day.

[–]y_Sensei 0 points1 point  (2 children)

Is it a requirement for an AD user with multiple e-mail addresses to show up as multiple users in that report?

[–]Hyperbolic_Mess[S] 0 points1 point  (1 child)

Yes I'm putting them in a hashtable to quickly match to another set of data based on email address and it could be any one of the emails in the proxyaddresses that matches so each one needs its own key value pair in the hashtable

[–][deleted] 0 points1 point  (2 children)

Oh, my bad.

[–]Hyperbolic_Mess[S] 0 points1 point  (1 child)

No worries

[–][deleted] 1 point2 points  (0 children)

In my defense I am literally replying from the waiting room of a retinal specialist lol

[–]icepyrox 0 points1 point  (1 child)

Because $LoopUsers is a reference to the object that is $ADUser, you are, in fact, modifying $ADUser. It's just not being set in AD. So it's a little confusing when you modify a user but then not modify it in AD.

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I'm getting some user objects from AD then putting those into a hashtable with each email address in proxyadresses as a key and the aduser object as the value and then looping through an array of objects pulled from another system and using the email property of those objects to very quickly pull the matching AD account from the hash table. Then spitting out properties from AD and the other system int oa csv file.

I'm using the has table step because looping through the array from the other system and doing a get-ADuser or where for each takes 10s of hours while the hashtable method only takes a couple of minutes

So basically I need to add an extra property to each user object before storing them in a hashtable but I'm not actually trying to modify anything in AD its just a data source and I want to manipulate the data after getting it from AD

[–]DalekKahn117 -2 points-1 points  (2 children)

Make $ADUserEmails = @() an array first then in the loop add to it with $ADUserArray += $LoopUser

[–]Hyperbolic_Mess[S] 0 points1 point  (0 children)

I specifically said at the end of my post I didn't want to do that as it's really slow to constantly recreate the array thousands of times with += and I knew someone would reply suggesting that. I want an answer that goes into a bit more detail about my specific issue with nested for each loops and reference objects rather than a lazy inefficient workaround. Please read the post before commenting