powershellactive-directorypowershell-7.0

Why can't I store my object in a variable within a Parallel ForEach-Object loop?


I'm trying to retrieve the AD property LastLogon from each of our domain controllers, for each user. Due to the amount of time this will take, I'm trying to use the Parallel feature from PowerShell 7

Example

$users = Get-ADUser -Filter "[filter]"
$DCs = Get-ADDomainController -Filter * | Where-Object { $_.Site -in $using:sites }

$users | ForEach-Object -ThrottleLimit 2  -Parallel {    
    $serv = $using:DCs

    foreach ($DC in $serv) {
        $lastLogonAD = Get-ADUser -Identity $_ -Properties LastLogon -Server $DC -ErrorAction Stop | Select-Object -ExpandProperty LastLogon

        $lastLogonConverted = [datetime]::FromFileTimeUTC($lastLogonAD)
    }
}

Error

Get-ADUser: 
Line |
   7 |  …        $lastLogonAD = Get-ADUser -Identity $_ -Properties LastLogon -Server  …
     |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | The server has returned the following error: invalid enumeration context.

What I've found is that if I remove the -Server parameter, it works, or if I don't store the property into a variable, it works.

Removing the server parameter
    foreach ($DC in $DCs) {
        $lastLogonAD = Get-ADUser -Identity $_ -Properties LastLogon -ErrorAction Stop | Select-Object -ExpandProperty LastLogon

        $lastLogonConverted = [datetime]::FromFileTimeUTC($lastLogonAD)
    }

Not storing the variable

    foreach ($DC in $DCs) {
        Get-ADUser -Identity $_ -Properties LastLogon -Server $DC -ErrorAction Stop | Select-Object -ExpandProperty LastLogon

        $lastLogonConverted = [datetime]::FromFileTimeUTC($lastLogonAD)
    }

Solution

  • The error "The server has returned the following error: invalid enumeration context." is mainly because your code is very inefficient, this error occurs on queries running for more than 30 minutes. See TechNet article Active Directory Troubleshooting: server has returned the following error - invalid enumeration context.

    Your parallel loop should be running per Domain Controller instead of per User. The parallel invocations should be processing all users per runspace.

    What below code does:

    1. Get a list of DistinguishedNames that will be processed in parallel.
    2. Start parallel invocations per Domain Controller and pass that list of DistinguishedNames to process per parallel invocation.
    3. Then group all results by DistinguishedName, sort each group by LastLogon and pick user on each group with the latest LastLogon.
    4. Lastly, project the object with a constructed "friendly" property LastLogonDate.
    $sites = # needs to be defined here...
    
    # only need the users `DistinguishedName` here
    $users = (Get-ADUser -Filter '[filter]').DistinguishedName
    
    # parallel loop per DC instead of per user
    Get-ADDomainController -Filter * |
        Where-Object { $_.Site -in $sites } |
        ForEach-Object -Parallel {
            $using:users | Get-ADUser -Properties LastLogon -Server $_
        } |
        Group-Object DistinguishedName |
        ForEach-Object {
            $_.Group | Sort-Object LastLogon -Descending -Top 1 |
                Select-Object *, @{ N='LastLogonDate'; E={ [datetime]::FromFileTimeUtc($_.LastLogon) }}
        }
    

    If you really need to have a reference of the Source DC per user then the way around it is to recreate the objects with a new SourceDC property inside the parallel loop, for instance:

        # No code changes before this
        ForEach-Object -Parallel {
            $sourceDC = $_
            $using:users | Get-ADUser -Properties LastLogon -Server $_ |
                Select-Object *, @{ N='SourceDC'; E={ $sourceDC.Name }}
        }
        # No code changes after this
    

    If you're still facing the same issue after this change then you will need to reduce the number of users you're querying. Or perhaps you can try with a single call to the DCs using an elaborate LDAP Filter:

    # create one big ldap filter
    $filter = -join @(
        '(|'
        (Get-ADUser -Filter '[filter]').DistinguishedName |
            ForEach-Object { "(distinguishedName=$_)" }
        ')'
    )
    
    Get-ADDomainController -Filter * |
        Where-Object { $_.Site -in $sites } |
        ForEach-Object -Parallel {
            # then using this filter we can make a single call per DC
            Get-ADUser -LDAPFilter $using:filter -Properties LastLogon -Server $_
        } |
        # rest of the code stays the same here...