Implement Pipeline Support by making proper use of ValueFromPipeline and ValueFromPipelineByPropertyName in PowerShell functions

In our previous post, we learned how to make use of Begin, Process and End blocks to implement proper pipeline support. Building on to that, in this blog post, we’ll learn further about when to make use of ValueFromPipeline property and when to make use of ValueFromPipelineByPropertyName and what happens behind the scenes.

Multiple parameters that accept Pipeline Input

What if there are multiple parameters that can accept pipeline input and we need to run the Process block for them. Let’s consider below code:

Function Show-ProcessID {
   [CmdletBinding()]
   Param(
      [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
      [String] $Id,

      [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
      [String] $ProcessName
   )

   Begin {
      Write-Verbose "In the begin block"
   }

   Process {
      Write-Verbose "In the process block"
      Write-Host "$ProcessName has process Id as $Id"
   }

   End {
      Write-Verbose "In the end block"
   }
}

If we run above code, we can see output like below:

accept multiple parameters as pipeline input

It looks like it is processing the same value for each parameter just because it accepted pipeline input. We can get around this issue by using Property as ValueFromPipelineByPropertyName. Note that when using this, it should match the property name in incoming object and our function. Example of our new code:

Function Show-ProcessID {
   [CmdletBinding()]
   Param(
      [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
      [String] $Id,

      [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
      [String] $ProcessName
   )

   Begin {
      Write-Verbose "In the begin block"
   }

   Process {
      Write-Verbose "In the process block"
      Write-Host "$ProcessName has process Id as $Id"
   }

   End {
      Write-Verbose "In the end block"
   }
}

If we pass the same input again, we can now see it is being parsed correctly:

accept multiple parameters as pipeline input-2

Another option to achieve same kind of output would be to use ParameterSets, but that would mean that you would only have one parameter or the other to accept pipeline input and work around it.

Order of Parameter Binding Process From Pipeline

Let’s modify our code to accept input from both ValueFromPipeline=$true and ValueFromPipelineByPropertyName=$true:

Function Show-ProcessID {
   [CmdletBinding()]
   Param(
      [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
      [String] $Id,

      [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
      [String] $ProcessName
   )

   Begin {
      Write-Verbose "In the begin block"
   }

   Process {
      Write-Verbose "In the process block"
      Write-Host "$ProcessName has process Id as $Id"
   }

   End {
      Write-Verbose "In the end block"
   }
}

Now, the output will be like below:

accept multiple parameters as pipeline input

Note that this is same as the case where we did not specified ValueFromPipelineByPropertyName=$true. This is the due to the order of binding inputs when using both of these attributes.

Below are the rules to decide:

  1. Bind parameter by Value with same Type (No Coercion)
  2. Bind parameter by PropertyName with same Type (No Coercion)
  3. Bind parameter by Value with type conversion (Coercion)
  4. Bind parameter by PropertyName with type conversion (Coercion)

We can use Trace-Command to dig deeper and see what is happening:

$Process = Get-Process | Select -First 1
Trace-Command parameterbinding { $Process | Show-ProcessID} -PSHost

In case of ValueFromPipelineByPropertyName=$true:

Using trace-command to observe parameter binding order

We can see that it has first started with ‘NO COERCION’ and binded it successfully. Then it tried it with ‘COERCION’ and binded it successfully. Finally it decided that since both are same, there is no need for ‘COERCION’.

If we would have used both ValueFromPipeline=$true and ValueFromPipelineByPropertyName=$true, the trace-command would change to like below:

Using trace-command to observe parameter binding order-3.PNG

Note that it first tries for PIPELINE INPUT ValueFromPipeline as ‘NO COERCION’ and then it went for ValueFromPipelineByPropertyName as ‘NO COERCION’. After this, it went for ValueFromPipeline with ‘COERCION’ (although that was not highlighted due to lazyness)

Working with the [Alias()] attribute

Let’s say that our parameter names does not match with the incoming property names successfully. Or there may be possible more than one type of incoming objects. In such a case, we can use alias to perfectly match the property from incoming objects.

Let’s modify our code to look like below:

Function Show-ProcessID {
   [CmdletBinding()]
   Param(
      [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
      [Alias("Id")]
      [String] $ProcessID,

      [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
      [Alias("Name")]
      [String] $ProcessName
   )

   Begin {
      Write-Verbose "In the begin block"
   }

   Process {
      Write-Verbose "In the process block"
      Write-Host "$ProcessName has process Id as $ProcessID"
   }

   End {
      Write-Verbose "In the end block"
   }
}

With Trace-Command output as below:

Using trace-command to observe parameter binding order

which is not terribly different from the one in previous case. So when using aliases, Trace-Command is not much useful for debugging.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s