2

I am using [adsisearcher] to grab AD User info because it's way faster than get-aduser. I am also trying to figure out how to add it into a hashtable or dictionary using LINQ or perhaps any other alternative that is fast. For me I'm trying to get the best performance/most efficiency because long term goal is importing the data into a contact list.

Here is what I currently have, and this works well, but I'm curious if there is a quicker way to organize the data?

$Start = Get-Date

$searcher=[adsisearcher]""
$searcher.Sort.PropertyName = "sn"
$searcher.Filter = "(&(extensionAttribute2=customValue)(|(mobile=*)(telephonenumber=*)))"
$colProplist = "givenname","extensionattribute2","initials","mobile","telephonenumber","sn","displayname","company","title","mail","department","thumbnailphoto","samaccountname"
foreach ($i in $colPropList){$searcher.PropertiesToLoad.Add($i) > $NULL }

$End = Get-Date

Write-Host "User info took $($Start - $End) seconds"
Write-Host ""

#----------------------------------------------------

Write-Host "Creating User Hashtable"
$Start = Get-Date

$Users=@{}

$Users = $searcher.FindAll() | ForEach-Object{
    New-Object -TypeName PSCustomObject -Property @{

        'FirstName' = $_.properties.givenname -join ''
        'MiddleName' = $_.properties.initials -join ''
        'LastName' = $_.properties.sn -join ''
        'DisplayName' = $_.properties.displayname -join ''
        'SamAccountName' = $_.properties.samaccountname -join ''
        'Email' = $_.properties.mail -join ''
        'Mobile' = $_.properties.mobile -join ''
        'TelephoneNumber' = $_.properties.telephonenumber -join ''
        'Title' = $_.properties.title -join ''
        'Dept' = $_.properties.department -join ''
        'Company' = $_.properties.company -join ''
        'Photo' = $_.properties.thumbnailphoto -join ''
        'ExtensionAttribute2' = $_.properties.extensionattribute2 -join ''

     } | Select-Object -Property FirstName, MiddleName, LastName, DisplayName, SamAccountName, Email, Mobile, TelephoneNumber, Title, Dept, Company, Photo, ExtensionAttribute2
 }

$End = Get-Date

Write-Host "User HashTable took $($Start - $End) seconds"
Write-Host ""

I heard using LINQ is very quick, but I get stuck in properly using the syntax and I can't seem to find good documentation or examples of how to use it.

Koobah84
  • 185
  • 2
  • 12
  • 1
    I don't see much room for improvement to your code using `[adsisearcher]`, ***maybe*** adding `(objectcategory=person)(objectclass=user))"` could improve the search a bit (?). Also, casting `[pscustomobject]@{...}` should be faster than using `New-Object` and using a classic `foreach` loop is definitely faster than `foreach-object` though it will consume a ***LOT*** of memory depending on the number of users... What is the reason for all the `-join ''`? – Santiago Squarzon Jun 04 '21 at 22:27
  • 1
    Thank you for the comment. When I actually did testing between foreach and foreach-object; surprisingly, I had a bit of a faster result with the foreach-object. Regarding the joins, maybe they're unnecessary. I was just looking at examples and someone had it so I incorporated it into my script. I'll remove it and see if it changes anything. Also I noticed if I do something like this: FirstName = (($_.properties).givenname | Out-String) This significantly slows down the code, but then the output is a lot cleaner (in columns) which I like more, just not for performance loss. – Koobah84 Jun 04 '21 at 22:34
  • 1
    I see why the `-join ''` is needed, my bad. What you ***could*** do, supposing that **user objects** are distributed in different OUs is to get all OUs and then use a `runspace pool` or the `ThreadJob module` to process chunks of OUs. Btw, the `Select-Object` doesn't seem to have any use there, you can remove it. – Santiago Squarzon Jun 04 '21 at 22:39
  • 1
    Sending anything through the pipeline dramatically decreases the speed of what you're testing. I have to agree with Santi here in using the foreach loop. Using `PSCustomObject` definitely is a faster alternative than using the old method of `New-Object`. Its computationally expensive creating that object, then sending it over to `Select-Object` over the pipeline so, `PSCustomObject` will take the load off of that. – Abraham Zinala Jun 04 '21 at 22:42
  • Thanks for the responses. Sorry if this may sound ignorant on my part, but I can't seem to visually see what the difference is if I take out the join or leave it in my code. What does it do in my case? Also regarding the PSCustomObject.. Isn't that what I am using here: New-Object -TypeName *PSCustomObject* -Property @{ Also regarding using runspace pools. I completely agree with you, and I would like to incorporate this, but I haven't had much luck doing this. Every example I have seen is a bit different, and I can't figure out how I would be able to incorporate it into my code. – Koobah84 Jun 04 '21 at 23:07

1 Answers1

3

So, as in my comments, I personally don't see much room for improvement to your script. I have modified some minor things. You should test with measure-command and see if the code runs faster with a classic foreach loop or with a foreach-object loop.

I also changed the -join '' for strong typed [string], which I'm not sure, but might be faster.

To answer your question, why the -join '' is there, this is because each property is of the type ResultPropertyValueCollection

IsPublic IsSerial Name                          BaseType                                 
-------- -------- ----                          --------                                 
True     False    ResultPropertyValueCollection System.Collections.ReadOnlyCollectionBase

By doing a -join '' you're converting the value of each property to a string:

PS /> ($z.samaccountname -join '').GetType()

IsPublic IsSerial Name   BaseType
-------- -------- ----   --------
True     True     String System.Object

PS /> ([string] $z.samaccountname).GetType()

IsPublic IsSerial Name   BaseType
-------- -------- ----   --------
True     True     String System.Object  

But why is this important?
Well, very basically, if you attempt to export the data without converting those values to string to a Csv for example you would end up with something like this (you would see the type of each value instead):

enter image description here

$start = Get-Date
$end   = Get-Date

$searcher = [adsisearcher]::new()
$searcher.Sort.PropertyName = "sn"
$searcher.Filter = "(&(objectcategory=person)(objectclass=user)(extensionAttribute2=*)(|(mobile=*)(telephonenumber=*)))"

$colProplist = @(
    'givenname', 'extensionattribute2'
    'initials', 'mobile', 'telephonenumber'
    'sn', 'displayname', 'company'
    'title', 'mail', 'department'
    'thumbnailphoto', 'samaccountname'
)
$searcher.PropertiesToLoad.AddRange($colProplist)

Write-Host "User info took $($Start - $End) seconds"
Write-Host ""
Write-Host "Creating User Hashtable"

$users = foreach($user in $searcher.FindAll()) {
    $i = $user.properties

    [pscustomobject]@{
        FirstName           = [string] $i.givenname
        MiddleName          = [string] $i.initials 
        LastName            = [string] $i.sn 
        DisplayName         = [string] $i.displayname 
        SamAccountName      = [string] $i.samaccountname 
        Email               = [string] $i.mail 
        Mobile              = [string] $i.mobile 
        TelephoneNumber     = [string] $i.telephonenumber 
        Title               = [string] $i.title 
        Dept                = [string] $i.department 
        Company             = [string] $i.company 
        Photo               = [string] $i.thumbnailphoto 
        ExtensionAttribute2 = [string] $i.extensionattribute2
    }
}
$End = Get-Date

Write-Host "User HashTable took $($Start - $End) seconds"
Write-Host ""
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Thank you for your answer and your help. That makes sense. 1 follow up question; once I have that data, do you know how I can incorporate it into a runspacepool to import that information into a users mailbox? I'm currently doing this using Microsoft Graphics API, but I am open to doing it using EWS or EWS API as well. If it's easier I can create a new topic for that. Thank you! – Koobah84 Jun 05 '21 at 00:07
  • @Koobah84 I honestly don't have experience related to this question, I think it's best if you post a new question with this and probably will get the best answer. Regarding runspace pool, I use [this](https://stackoverflow.com/questions/41796959/why-powershell-workflow-is-significantly-slower-than-non-workflow-script-for-xml) great answer as my blueprint for whenever I would need to use runspace pool. – Santiago Squarzon Jun 05 '21 at 00:19