question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Parameter binding pipeline input, and how the default parameter set influences that behavior

See original GitHub issue

When you have multiple parameter sets that take pipeline input, one by value and another (or others) by property name, if you pipe in the actual object type that is accepted by value that will not necessarily result in the corresponding parameter set being used. This feels wrong, more like a gotcha than how things should actually work.

For example, consider Enable-PSBreakpoint, Disable-PSBreakpoint, and Remove-PSBreakpoint.

Each of those commands has two parameter sets. Let’s just call them “Id” and “Breakpoint” (those may be their actual names, but it doesn’t matter). The “Id” parameter set has an int id parameter that accepts pipeline input by property name. The “Breakpoint” parameter set has a Breakpoint breakpoint parameter set that accepts pipeline input by value.

Enable-PSBreakpoint is configured to use the “Id” parameter set by default. The others are configured to use the “Breakpoint” parameter set by default.

If you pipe a Breakpoint object into each of these cmdlets, the parameter set that is chosen is based on the default parameter set and the properties on the object passed in. For Enable-PSBreakpoint, the “Id” parameter set is a match because the incoming object has an “Id” property, so it is used. For the others, the “Breakpoint” parameter set is a match, so it is used.

I think this is wrong, and feel that if you pipe in an object that matches exactly the type of a parameter that accepts pipeline input by value, that is the most logical choice to use no matter what the default parameter set is. You can only have one ValueFromPipeline parameter in a command – that should be for a good reason like the behavior expected here, but it doesn’t really seem like it is.

As a command author, if I were building these commands from scratch I would want the “Id” parameter set to be the default so that ad hoc users are prompted for an id if they invoke the command with no parameters (prompting them for a breakpoint with no transform attribute to convert from an id to a breakpoint is useless), but I would also most definitely expect that the “Breakpoint” parameter set is used if I pipe in an actual “Breakpoint” object.

I suspect at this point this would just be a breaking change. I’m posting this issue here just the same because it can cause unexpected bugs to occur (like the one I just lost an hour to), so users should be aware of this (Is this behavior documented? I’ll have to check later). This is also one of those things that I would be inclined to opt into in my modules if I could localize the change so that my commands work more intelligently (i.e. if we had support for optional features as described in an open RFC right now).

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:3
  • Comments:7 (4 by maintainers)

github_iconTop GitHub Comments

2reactions
KirkMunrocommented, Sep 6, 2019

This issue was bothering me again today. I would bet that it hurts performance when using pipelines too, because of a common design pattern.

It is very common to write a command that accepts an object on one parameter set and values that can be used to retrieve that object on other parameter sets.

It is also common with that design pattern to set the default parameter set as one of the parameter sets that accepts a value that can be used to retrieve an object, so that users are prompted to enter that value if they invoke the command with no parameters. This is actually a requirement if you want to support automatic prompting for user input because unless you use a transformation attribute, users cannot input an object instance when they are prompted for one.

If you follow that design pattern, then even when you pass in the actual object you want to modify, the command will go look up the object again, which could be expensive.

Here’s how you can see this in action.

  1. Define this function:

    function Test-ParameterBinder {
        [CmdletBinding(DefaultParameterSetName='ByPropertyName')]
        param(
            [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='ByPropertyName')]
            [ValidateRange(1,[int]::MaxValue)]
            [int]
            $Id,
    
            [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ByValue')]
            [ValidateNotNull()]
            [System.Diagnostics.Process]
            $Process
        )
        begin {
            "Bound parameter set in begin: $($PSCmdlet.ParameterSetName)"
        }
        process {
            "Bound parameter set in process: $($PSCmdlet.ParameterSetName)"
        }
    }
    

    That function uses the common design pattern that I was talking about. One command that takes pipeline input by value or by property name, with the default parameter set configured to allow users to invoke the command successfully even if they don’t provide any pipeline input or parameters. In this case, they would be prompted for an ID and could enter a process ID to make it work.

  2. Now let’s run that function a few times.

    $p = Get-Process -Id $pid
    $p | Test-ParameterBinder
    [pscustomobject]@{Id=$PID} | Test-ParameterBinder
    

    This outputs the following from the first invocation:

    Bound parameter set in begin: ByPropertyName
    Bound parameter set in process: ByPropertyName
    

    It also outputs the following from the second invocation:

    Bound parameter set in begin: ByPropertyName
    Bound parameter set in process: ByPropertyName
    

    The problem is very clear here: even when you pipe in the actual object you want to work with, the ByPropertyName parameter set is bound to the invocation, which would very often result in lookup of the object that you had in the first place. Some function authors work around this by actually looking at $_ in the process block of a function, checking if it is the object type they need, and if so, just grabbing it to avoid the extra lookup. I’ve done this a lot in the past to try to keep things performant. That approach does not work in cmdlets though, and in either case if you pipe in an object and there is a ValueFromPipeline parameter in one of the possible parameter sets that matches the type exactly, that parameter set should be identified as most appropriate among the parameter sets that are available.

  3. Now let’s redefine the function by changing its default parameter set.

    function Test-ParameterBinder {
        [CmdletBinding(DefaultParameterSetName='ByValue')]
        param(
            [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName='ByPropertyName')]
            [ValidateRange(1,[int]::MaxValue)]
            [int]
            $Id,
    
            [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='ByValue')]
            [ValidateNotNull()]
            [System.Diagnostics.Process]
            $Process
        )
        begin {
            "Bound parameter set in begin: $($PSCmdlet.ParameterSetName)"
        }
        process {
            "Bound parameter set in process: $($PSCmdlet.ParameterSetName)"
        }
    }
    
  4. Then invoking it a few more times with pipeline input:

    $p | Test-ParameterBinder
    [pscustomobject]@{Id=$PID} | Test-ParameterBinder
    
  5. Now you see the following results:

    Bound parameter set in begin: ByValue
    Bound parameter set in process: ByValue
    Bound parameter set in begin: ByValue
    Bound parameter set in process: ByPropertyName
    

    As you can see from those results, the parameter set that is bound when the actual object is passed in via the pipeline is ByValue. This is desired, and keeps things performant. Also, when you pass in an object that does not match the object type that is bound by value, PowerShell falls back to pipeline input by property name, finds a match there, and selects the ByPropertyName parameter set. That’s also desired and expected.

    The downside is that if you invoke Test-ParameterBinder with no parameters and without pipeline input, you’ll be prompted to provide it with a value for the Process parameter, but you cannot do that.

    The other issue is that programmatically, when you are in the begin block, it looks like the parameter set is ByValue because that is the default parameter set and PowerShell hasn’t yet been able to determine what the parameter set will be since it has not started processing pipeline input yet. I wish the parameter set name was $null in this case, because it is not known, and command authors should know that it is not known so that they can write their code accordingly. Sometimes you want to do specific things based on the parameter set that is bound, and if it is not bound/known because there are multiple possibilities in the begin block, then you shouldn’t take action based on the parameter set name at that point. In hindsight, it would have been better to have a specific PSCmdlet method that gets invoked each time the parameter set is bound, so that appropriate work can be done the moment a parameter set is bound and each time it is bound, whether than happens in begin or process.

Of course, all of this analysis is moot if we can’t do a thing about it.

@daxian-dbw Any thoughts that you would like to contribute to this?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Understanding PowerShell pipeline parameter binding
By setting a parameter to accept pipeline binding by property name, we can tell PowerShell to map only a single property of an...
Read more >
Default Parameter - an overview
A default parameter is one that need not necessarily be provided by the caller; if it is missing, then a preestablished default value...
Read more >
about Parameter Sets - PowerShell
Default parameter sets ​​ PowerShell uses the default parameter set when it can't determine the parameter set to use based on the information ......
Read more >
Powershell Parameter binding ByPropertyName and ...
The ByPropertyName binding means that the parameter looks for a property with that name in the input object. The String object doesn't have...
Read more >
Parameter Binding concepts in PowerShell - ScriptRunner
In this blog post I will be going over two fundamental PowerShell pipeline parameter binding concepts: “byValue” and “byProperty”.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found