Private/ActiveDirectory/Get-ADObjectsSequential.ps1

function Get-ADObjectsSequential {
    <#
    .SYNOPSIS
        Retrieves Active Directory objects from multiple search bases sequentially
 
    .DESCRIPTION
        Processes multiple AD search bases in sequence with comprehensive logging
        and error handling. Optimized for reliability over speed.
 
    .PARAMETER SearchBase
        Array of distinguished names to search
 
    .PARAMETER IncludeInherited
        Whether to include inherited permissions in security descriptors
 
    .PARAMETER CorrelationId
        Unique identifier for operation tracking and correlation
 
    .EXAMPLE
        PS> Get-ADObjectsSequential -SearchBase @("OU=Users,DC=contoso,DC=com", "OU=Groups,DC=contoso,DC=com")
 
        DESCRIPTION: Sequentially processes multiple organizational units
        OUTPUT: Combined collection of AD objects from all search bases
        USE CASE: Reliable bulk processing when parallel processing isn't required
 
    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
 
        TROUBLESHOOTING:
        - For performance optimization: .\Troubleshooting\Performance\Sequential-Optimization.md
        - For bulk operation issues: .\Troubleshooting\Common\Bulk-Processing.md
    #>


    [CmdletBinding()]
    [OutputType('Microsoft.ActiveDirectory.Management.ADObject')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]]$SearchBase,

        [Parameter()]
        [switch]$IncludeInherited,

        [Parameter()]
        [string]$CorrelationId = [System.Guid]::NewGuid().ToString()
    )

    begin {
        # Robust count calculation for test environment compatibility - store once to avoid pipeline issues
        $script:searchBaseCount = if ($SearchBase -is [array]) { $SearchBase.Count } else { 1 }
        Write-Verbose "Starting sequential AD object retrieval from $($script:searchBaseCount) search bases"

        # Log bulk operation start
        Write-ADOperationSecurityLog -OperationName 'Get-ADObjectsSequential' -Outcome 'Attempt' -SecurityContext @{
            SearchBaseCount = $script:searchBaseCount
            IncludeInherited = $IncludeInherited.IsPresent
            ProcessingType = 'Sequential'
            ADAccessType = 'BulkRetrieval'
        } -CorrelationId $CorrelationId

        # Initialize tracking variables
        $script:totalObjects = 0
        $script:successfulBases = 0
        $script:failedBases = 0
    }

    process {
        # Process each search base sequentially and track results
        $allResults = @()
        
        # The loop variable must not share the parameter's name in any letter case: variable names are
        # case-insensitive, so a $searchBase loop variable is the [string[]]-constrained $SearchBase
        # parameter, each element is re-wrapped as a one-element array, and the call below fails to
        # bind it to a [string] parameter. Discovery then returns nothing and logs one failure per base.
        foreach ($currentSearchBase in $SearchBase) {
            try {
                Write-Verbose "Processing search base: $currentSearchBase"
                $searchResults = Get-ADObjectFromSearchBase -SearchBase $currentSearchBase -IncludeInherited:$IncludeInherited -CorrelationId $CorrelationId
                
                if ($searchResults) {
                    $allResults += $searchResults
                    $script:successfulBases++
                    Write-Verbose "Successfully retrieved $(@($searchResults).Count) objects from $currentSearchBase"
                } else {
                    $script:successfulBases++  # No objects but no error
                    Write-Verbose "No objects found in $currentSearchBase (not an error)"
                }
            }
            catch {
                $script:failedBases++
                Write-Warning "Failed to process search base $currentSearchBase : $($_.Exception.Message)"
                
                # Log the failure but continue processing
                Write-ADOperationSecurityLog -OperationName 'Get-ADObjectsSequential' -Outcome 'Failure' -SecurityContext @{
                    FailedSearchBase = $currentSearchBase
                    ErrorMessage = $_.Exception.Message
                    ContinueProcessing = $true
                } -CorrelationId $CorrelationId
            }
        }

        $script:totalObjects = $allResults.Count
        
        # Use stored count to avoid pipeline parameter issues
        Write-Verbose "Sequential processing completed: $($script:totalObjects) objects from $($script:successfulBases)/$($script:searchBaseCount) search bases"

        # Output results to pipeline
        $allResults | Write-Output
    }

    end {
        # Use stored count to avoid pipeline parameter issues
        
        # Log bulk operation completion
        Write-ADOperationSecurityLog -OperationName 'Get-ADObjectsSequential' -Outcome 'Success' -SecurityContext @{
            SearchBaseCount = $script:searchBaseCount
            SuccessfulBases = $script:successfulBases
            FailedBases = $script:failedBases
            TotalObjectsRetrieved = $script:totalObjects
            IncludeInherited = $IncludeInherited.IsPresent
            ProcessingType = 'Sequential'
            ADAccessType = 'BulkRetrieval'
            ResultSummary = "Retrieved $($script:totalObjects) objects from $($script:successfulBases)/$($script:searchBaseCount) search bases"
        } -CorrelationId $CorrelationId
    }
}