About_PipelineBinding

One of the most powerful features of an object-oriented shell like PowerShell is that it can accept input from the pipeline based on the properties of the objects in the pipeline…

When you’re writing an advanced function in PowerShell and using the Parameter attribute to control binding values to the parameters, you can specify that you want a specific parameter to accept the full object that was output by a previous command in the pipeline, or that you want it to accept the value of a property of the object that matches the parameter name (or alias) ... or you can mix and match both. That’s what allows you to run code like this:

Get-Module | Get-ChildItem

Because the Path parameter of Get-ChildItem is set to take the value from the pipeline based on matching property names (that is, ValueFromPipelineByPropertyName is True) as well as just a straight string from the pipeline (ValueFromPipeline is also True), any object with a Path property can be piped to Get-ChildItem.

I used to think that ValueFromPipelineByPropertyName took precedence over ValueFromPipeline. It turns out that I was wrong. In a recent email exchange, someone pointed out that in Windows PowerShell in Action 2nd Ed., Bruce Payette said that ValueFromPipeline takes precedence over ValueFromPipelineByPropertyName (see table 2.3 in section 2.5.2). However, that’s not the whole story (and I think that table is wrong).

It turns out that when an object is piped into a command, PowerShell tries to bind the object without type coercion first, and only if that fails does it try type coercion. In case you’ve never heard that phrase before, “type coercion” is when an object is changed from one type of object into another. In each case (without coercion and with), PowerShell does in fact give ValueFromPipeline precedence over ValueFromPipelineByPropertyName…

Perhaps An Example Can Clear Things Up

As an example, in this function I defaulted everything so that the ValueFromPipeline parameter should take precedence, even down to setting the “Value” parameter set to be the default. Since PowerShell can coerce pretty much any object to a string, that means you can pipe anything to this, and it will bind to the $Name parameter as the ValueFromPipeline. However, if the object has a string property named Mode

function test-binding {
   [CmdletBinding(DefaultParameterSetName='Value')]
   param (
      [Parameter(ValueFromPipelineByPropertyName=$true, ParameterSetName='PropertyName')]
      [String]$Mode,
   
      [Parameter(ValueFromPipeline=$true, ParameterSetName='Value')]
      [String]$Name
   )
   process {
      if(!($Result = $Name)){ $Result = $Mode }
      Write-Host "Got '$Result' via $($PSCmdlet.ParameterSetName)"
   }
}

# Will output the MODE (ValueFromPipelineByPropertyName)
get-item c:\windows | test-binding

If you execute that code you can see that the output seems to show that ValueFromPipelineByPropertyName took precedence. What happened is that the ValueFromPipeline would have required a cast (the item is a DirectoryInfo object, not a string), but the ValueFromPipelineByPropertyName doesn’t require a cast (because the “Mode” property is a string representation of the Attributes).

You can see what happens more clearly if you use Trace-Command to see the ParameterBinding:

$Dir = Get-Item C:\Windows
Trace-Command -Name ParameterBinding -Option All -Expression {
   $Dir | Test-Binding
} -PSHost

# I'll skip over the mandatory parameter check
# Here is the relevant output:
BIND PIPELINE object to parameters: [test-binding]
    PIPELINE object TYPE = [System.IO.DirectoryInfo]
    RESTORING pipeline parameter’s original values
    Parameter [Name] PIPELINE INPUT ValueFromPipeline NO COERCION
     BIND arg [C:\Windows] to parameter [Name]
        Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
            result returned from DATA GENERATION: C:\Windows
        BIND arg [C:\Windows] to param [Name] SKIPPED
    Parameter [Mode] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
     BIND arg [d——] to parameter [Mode]
        Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
            result returned from DATA GENERATION: d——
        BIND arg [d——] to param [Mode] SUCCESSFUL

If we changed the [String]$Mode parameter to make it [DateTime]$LastAccessTime instead, you will see the same thing happening: it can’t bind the Name from the pipeline without casting, so it tries the other parameter based on the property name, and since it doesn’t require a cast, it’s value is accepted successfully.

If we changed the [String]$Mode parameter be [String]$LastAccessTime then that parameter will require casting also, and you’ll see that it gets skipped in the first pass too, and the $Name parameter will win when PowerShell does it’s second pass at binding using type coercion (casting).

In other words, by observation from the Trace-Command output, what PowerShell does when it’s binding objects from the pipeline is that it tries four different ways, in this order:

  1. PIPELINE INPUT ValueFromPipeline NO COERCION
  2. PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
  3. PIPELINE INPUT ValueFromPipeline WITH COERCION
  4. PIPELINE INPUT ValueFromPipelineByPropertyName WITH COERCION
  1. .

P.S.: As a bonus tip: ParameterSets can only be determined by binding a unique parameter, not by how that parameter is bound, so you can’t use ValueFromPipeline vs ValueFromPipelineByPropertyName vs Positional or named parameters to distinguish parameter sets … and you can only have as many ParameterSets as you do unique parameters that go in them. However, as a workaround you might consider (in the case of pipeline binding) adding a unique throw-away parameter that maps to a property (that you don’t care about) of an object you’re expecting…

Similar Posts:

    None Found

4 thoughts on “About_PipelineBinding”

  1. Holy crap, they fixed it! Back in PowerShell 2.0, pipeline binding by value (with coercion) took precedence over binding by property name. For example, try changing your test-binding function so that the same string parameter can bind either way, and call it like this:

    function test-binding
    {
        [CmdletBinding()]
        param (
            [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
            [string]
            $Mode
        )

        process
        {
            Write-Host "`$Mode = '$Mode'"
        }
    }

    $objects = 'Name', [pscustomobject] @{ Mode = 'Mode' }

    $objects | test-binding

    In PowerShell 2.0, you’d get this output:

    $Mode = 'Name'
    $Mode = 'System.Collections.Hashtable'

    In 4.0, it’s giving a much better result:

    $Mode = 'Name'
    $Mode = 'Mode'

    I don’t have a system with only PowerShell 3.0 handy on it right now to test, but I’m pretty sure that this change is new to v4.0.

    1. Oops, that test isn’t quite right; I left a reference to [pscustomobject] which didn’t exist in PowerShell 2.0. I’ll keep playing with it and see if I can reproduce my previous results.

      What I remember concluding before (whenever I did these tests last time, in v2.0 or 3.0) was that there was no reason to ever use a combination of both ByValue and ByPropertyName if the parameter’s type was [string], since ByValue with coercion seemed to always win.

      1. Yeah, your first comment should have used:

        $objects = 'Name', (New-Object PSObject -Property @{ Mode = 'Mode' })

        Then it would work the same in any version. I understand the feeling you have that this didn’t work right before (I felt the same way when I first realized that it doesn’t work the way PowerShell In Action describes it: that the value comes before the property once you’re coercing), but after several rounds of examples, I think it’s always been the way it is now.

  2. I know I’ve run into some situation before where I defined a parameter as binding both ways, expecting it to work by property name, and instead I wound up with the input object’s type name in my string parameter (the default Object.ToString() value), but I can’t seem to reproduce it now. Everything I’ve tried today has worked perfectly on both PowerShell 2.0 and 4.0 (with “by property name” binding taking effect before “by value with coercion”.)

Comments are closed.