$PSBoundParameters, Pipeline and the ValueFromPipelineByPropertyName parameter attribute

While using and abusing $PSBoundParameters in one of the my functions, I’ve encountered some strange and unexpected behaviour. If you pass an object via pipeline to a function which accepts parameters from an object properties using ValueFromPipelineByPropertyName, Powershell re-binds $PSBoundParameters only for existing object properties. If object property is not exists in the next pipeline loop, $PSBoundParameters will keep the key from the previous iteration.

Example:

Function Test-PSBoundParameters
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        $ParamA,
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        $ParamB
    ) 

    Process
    {
        '_______________________'
        "Bound 'ParamA': $($PSBoundParameters.ParamA)"
        "Variable 'ParamA': $ParamA"
        "Bound 'ParamB': $($PSBoundParameters.ParamB)"
        "Variable 'ParamB': $ParamB"
    }
} 

$Test = @{ParamA = 'ParamA' ; ParamB = 'ParamB'}, @{ParamA = 'ParamA'}

$Test | ForEach-Object {New-Object psobject -Property $_} | Test-PSBoundParameters

What I expect:

_______________________
Bound 'ParamA': ParamA
Variable 'ParamA': ParamA
Bound 'ParamB': ParamB
Variable 'ParamB': ParamB
_______________________
Bound 'ParamA': ParamA
Variable 'ParamA': ParamA
Bound 'ParamB':
Variable 'ParamB':

What really happens:

_______________________
Bound 'ParamA': ParamA
Variable 'ParamA': ParamA
Bound 'ParamB': ParamB
Variable 'ParamB': ParamB
_______________________
Bound 'ParamA': ParamA
Variable 'ParamA': ParamA
Bound 'ParamB': ParamB
Variable 'ParamB':

As you can see, the second object in pipeline doesn’t have the ParamB property. I expected that both ParamB variable an ParamB key in $PSBoundParameters in that case wouldn’t be assigned. But what actually happens, is that PowerShell keeps stale key for ParamB from the previous object in the next pipeline loop.

I’ve found this thread on the Windows PowerShell forums about this issue. One of the proposed solutions was to clear $PSBoundParameters on every iteration using $PSBoundParameters.Clear() or rely solely on corresponding variables. But clearing $PSBoundParameters breaks pipeline and I wanted to be able to use both $PSBoundParameters and variables.

The only solution I was able to come with, is to manually check if $PSBoundParameters keys’ values are the same as corresponding variables and if not – remove those keys. The comaprison uses the Object.Equals method if it’s available:

        $StaleKeys = $PSBoundParameters.GetEnumerator() |
                        ForEach-Object {
                            if($_.Value.PSobject.Methods.Name -match '^Equals$')
                            {
                                # If object has Equals, compare bound key and variable using it
                                if(!$_.Value.Equals((Get-Variable -Name $_.Key -ValueOnly -Scope 0)))
                                {
                                    $_.Key
                                }
                            }
                            else
                            {
                                # If object doesn't has Equals (e.g. $null), fallback to the PowerShell's -ne operator
                                if($_.Value -ne (Get-Variable -Name $_.Key -ValueOnly -Scope 0))
                                {
                                    $_.Key
                                }
                            }

                        }
        $StaleKeys | ForEach-Object {[void]$PSBoundParameters.Remove($_)}

Here is the function with the quirk mitigated:

Function Test-PSBoundParameters{
[CmdletBinding()]
 	Param(
 	    [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
 	    $ParamA,
 	    [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
 	    $ParamB
    ) 
    Process
    {
        $StaleKeys = $PSBoundParameters.GetEnumerator() |
                        ForEach-Object {
                            if($_.Value.PSobject.Methods.Name -match '^Equals$')
                            {
                                # If object has Equals, compare bound key and variable using it
                                if(!$_.Value.Equals((Get-Variable -Name $_.Key -ValueOnly -Scope 0)))
                                {
                                    $_.Key
                                }
                            }
                            else
                            {
                                # If object doesn't has Equals (e.g. $null), fallback to the PowerShell's -ne operator
                                if($_.Value -ne (Get-Variable -Name $_.Key -ValueOnly -Scope 0))
                                {
                                    $_.Key
                                }
                            }

                        }
        $StaleKeys | ForEach-Object {[void]$PSBoundParameters.Remove($_)}

        '_______________________'
        "Stale Keys: $StaleKeys"
        '_______________________'
        "Bound 'ParamA': $($PSBoundParameters.ParamA)"
        "Variable 'ParamA': $ParamA"
        "Bound 'ParamB': $($PSBoundParameters.ParamB)"
        "Variable 'ParamB': $ParamB"
    }
}

$Test = @{ParamA = 'ParamA' ; ParamB = 'ParamB'}, @{ParamA = 'ParamA'}

$Test | ForEach-Object {New-Object psobject -Property $_} | Test-PSBoundParameters

And the result:

_______________________
Stale Keys: 
_______________________
Bound 'ParamA': ParamA
Variable 'ParamA': ParamA
Bound 'ParamB': ParamB
Variable 'ParamB': ParamB
_______________________
Stale Keys: ParamB
_______________________
Bound 'ParamA': ParamA
Variable 'ParamA': ParamA
Bound 'ParamB': 
Variable 'ParamB':

[Updated: 15 May 2015 – My previous approach to comparing $PsBoundParameters keys and variables using PowerShell’s -ne operator failed to work with anything but simple value types (such as strings or integers)]
In effect, comparison of the arrays, hashtables, objects and so on failed. But it seems that I’ve found a much better solution now: the Object.Equals method. Looks like it fits perfectly for this task, but in case I’ve missed something again, please let me know.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s