Parameter binding pipeline input, and how the default parameter set influences that behavior
See original GitHub issueWhen 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:
- Created 4 years ago
- Reactions:3
- Comments:7 (4 by maintainers)
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.
Define this function:
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.
Now let’s run that function a few times.
This outputs the following from the first invocation:
It also outputs the following from the second invocation:
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 aValueFromPipeline
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.Now let’s redefine the function by changing its default parameter set.
Then invoking it a few more times with pipeline input:
Now you see the following results:
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 theByPropertyName
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 theProcess
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 isByValue
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 thebegin
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 specificPSCmdlet
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 inbegin
orprocess
.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?
I guess it is here: https://github.com/PowerShell/PowerShell/blob/6b2690ef8e5368344fd2987fcce0629f7a52aa3a/src/System.Management.Automation/engine/CmdletParameterBinderController.cs#L3540-L3567