Powershell: ValueFromPipelineByPropertyName doesn't work with multiple paramsets.

Created on 11 May 2020  路  6Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

I have defined a cmdlet with two parametersets. Each paramset allows parameters to be passed from the pipeline by the propertyName. The cmdlet works fine if I pass the parameters directly. If I pipeline an object, Cmdlet always resolves the parameter set to Default instead of TestParamSet

CmdLet Code:

```c#
[Cmdlet("Get", "SampleCmdlet", DefaultParameterSetName = "Default")]
[OutputType(typeof(string))]
public class GetSampleCmdlet : PSCmdlet
{

    [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Default")]
    [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "TestParamSet")]
    public string ParamOne { get; set; }

    [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "TestParamSet")]
    public string ParamTwo { get; set; }


    protected override void ProcessRecord()
    {
        Console.Out.WriteLine($"ParamSet: {ParameterSetName}");
        Console.Out.WriteLine($"ParamOne: {ParamOne} ParamTwo: {ParamTwo}");

        WriteObject("Hello World!");
    }

}

Invoking the Cmdlet with parameters:
```powershell
PS /Users/> Get-SampleCmdlet -ParamOne "One"
ParamSet: Default
ParamOne: One ParamTwo: 
Hello World!

PS /Users/> Get-SampleCmdlet -ParamOne "one" -ParamTwo "two"
ParamSet: TestParamSet
ParamOne: one ParamTwo: two
Hello World!

PS /Users/> skumbham-mac:~ skumbham$ pwsh

Passing the Parameters through the pipeline. Powershell always resolves the parameterset to Default

$test=@{}
$test["ParamOne"]="pipeline-one"
$test["ParamTwo"]="pipeline-two"
$obj = [pscustomobject]$test

$obj | Get-SampleCmdlet

ParamSet: $Default
ParamOne: pipeline-one ParamTwo:
Hello World!

Expected behavior

ParameterSet should be TestParamSet

Actual behavior

ParameterSet defaults to Default paramset.

Environment data

PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.2.4
PSEdition                      Core
GitCommitId                    6.2.4
OS                             Darwin 18.7.0 Darwin Kernel Version 18.7.0: Tue Aug 20 16:57:14 PDT 2019; root:xnu-4903.271.2~2/RELEASE_X86_64
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Question Resolution-Answered WG-Engine

Most helpful comment

@kumbham

It seems like if we remove the DefaultParameterSetName, normal cmdlet invocation seems to be breaking. ...

PS /Users/> Get-SampleCmdlet
Get-SampleCmdlet : Parameter set cannot be resolved...

Because each parameter has the Mandatory attribute, the parameter binder considers there to be no viable parameter set.

I think using another unused parameter set name like None for DefaultParameterSetName might achieve the behavior you are expecting:

function TwoSets{
    [CmdletBinding(DefaultParameterSetName = 'None')]
    param ( $NoSet,
            [parameter(ParameterSetName = 'A', Mandatory = $true)]$A,
            [parameter(ParameterSetName = 'B', Mandatory = $true)]$B  )
    process { $PSCmdlet.ParameterSetName }
}

TwoSets        # None
TwoSets -A 'a' # A
TwoSets -B 'b' # B

The matter of creating a parameter set with no mandatory parameters at all was puzzling enough to me that it led to stackoverflow#43701900.

Note that this part of the parameter binder is the topic of some recent discussion on #11143 and #11237.

All 6 comments

/cc @mklement0 Maybe dup?

Here is a repro in PowerShell:

function Get-SampleCmdlet
{
    [CmdletBinding(DefaultParameterSetName = "Default")]

    param
    (
        [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName = "Default")]
        [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName = "TestParamSet")]
        [string]
        $ParamOne,

        [Parameter(Mandatory,ValueFromPipelineByPropertyName, ParameterSetName = "TestParamSet")]
        [string]
        $ParamTwo
    )
    process {
        "ParamSet: $($PSCmdlet.ParameterSetName)"
        "ParamOne: $ParamOne"
        "ParamTwo: $ParamTwo"
    }
}

[pscustomobject]@{
    ParamOne = 'pipeline-one'
    ParamTwo = 'pipeline-two'
} |
    Get-SampleCmdlet

Output:

ParamSet: Default
ParamOne: pipeline-one
ParamTwo: 

Removing DefaultParameterSetName = "Default" results in

ParamSet: TestParamSet
ParamOne: pipeline-one
ParamTwo: pipeline-two

Changing the call site to

[pscustomobject]@{
    ParamOne = 'pipeline-one'
    ParamTwo = 'pipeline-two'
} |
    Get-SampleCmdlet -ParamTwo 'named-two'

results in

ParamSet: TestParamSet
ParamOne: pipeline-one
ParamTwo: named-two

It's not obvious to me that affinity to the Default parameter set in the OP should be considered wrong. It seems reasonable to me that, when there are two viable parameter sets, the binder would choose the default parameter set.

@alx9r thanks for your response. It seems like if we remove the DefaultParameterSetName, normal cmdlet invocation seems to be breaking. here's the output after removing DefaultParameterSetName

PS /Users/> Get-SampleCmdlet
Get-SampleCmdlet : Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
At line:1 char:1
+ Get-SampleCmdlet
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidArgument: (:) [Get-SampleCmdlet], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,ocipstools_objectstorage.CmdLets.GetSampleCmdlet

Pipelining the object works fine though:

PS /Users/> $test=@{}
PS /Users/> $test["ParamOne"]="pipeline-one"
PS /Users/> $test["ParamTwo"]="pipeline-two"
PS /Users/> $obj = [pscustomobject]$test
PS /Users/> $obj | Get-SampleCmdlet
ParamSet: TestParamSet
ParamOne: pipeline-one ParamTwo: pipeline-two
Hello World!

Any other suggestions on how to fix this? @alx9r @iSazonov

Here's my suggestion

function Get-SampleCmdlet
{
    [CmdletBinding(DefaultParameterSetName = "Default")]

    param
    (
        [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
        [string]
        $ParamOne,

        [Parameter(Mandatory,ValueFromPipelineByPropertyName, ParameterSetName = "TestParamSet")]
        [string]
        $ParamTwo
    )
    process {
        "ParamSet: $($PSCmdlet.ParameterSetName)"
        "ParamOne: $ParamOne"
        "ParamTwo: $ParamTwo"
    }
}

Then we get

> [pscustomobject]@{ ParamOne = 'pipeline-one' ;  ParamTwo = 'pipeline-two'} |     SampleCmdlet

ParamSet: TestParamSet
ParamOne: pipeline-one
ParamTwo: pipeline-two

and with one field but not the second

[pscustomobject]@{ParamOne = 'pipeline-one'} |    Get-SampleCmdlet
ParamSet: Default
ParamOne: pipeline-one
ParamTwo: 

No input

>Get-SampleCmdlet
cmdlet Get-SampleCmdlet at command pipeline position 1
Supply values for the following parameters:
ParamOne: aaa
ParamSet: Default
ParamOne: aaa`

Optional param piped but mandatory missing

[pscustomobject]@{    ParamTwo = 'pipeline-two'} |    Get-SampleCmdlet
Get-SampleCmdlet : The input object cannot be bound because it did not contain the information required to bind all mandatory parameters:  ParamOne

Optional piped mandatory on the command line

[pscustomobject]@{    ParamTwo = 'pipeline-two'} |    Get-SampleCmdlet -ParamOne foo
ParamSet: TestParamSet
ParamOne: foo
ParamTwo: pipeline-two

@kumbham

It seems like if we remove the DefaultParameterSetName, normal cmdlet invocation seems to be breaking. ...

PS /Users/> Get-SampleCmdlet
Get-SampleCmdlet : Parameter set cannot be resolved...

Because each parameter has the Mandatory attribute, the parameter binder considers there to be no viable parameter set.

I think using another unused parameter set name like None for DefaultParameterSetName might achieve the behavior you are expecting:

function TwoSets{
    [CmdletBinding(DefaultParameterSetName = 'None')]
    param ( $NoSet,
            [parameter(ParameterSetName = 'A', Mandatory = $true)]$A,
            [parameter(ParameterSetName = 'B', Mandatory = $true)]$B  )
    process { $PSCmdlet.ParameterSetName }
}

TwoSets        # None
TwoSets -A 'a' # A
TwoSets -B 'b' # B

The matter of creating a parameter set with no mandatory parameters at all was puzzling enough to me that it led to stackoverflow#43701900.

Note that this part of the parameter binder is the topic of some recent discussion on #11143 and #11237.

Was this page helpful?
0 / 5 - 0 ratings