powershellparameter-sets

ParameterSetName detection in PowerShell functions matching on ValueFromPipeline input object type?


I'm seeing some strange behavior in a custom function I've written, and so I wrote some quick test functions with different characteristics to exhibit these behaviors. The problem arises when parameter sets are similar enough that the only differentiating factor is the type of an object received through the pipeline.

First, I made a simple type that serves only to be different than a string.

Add-Type @"
public class TestType {
   public string Prop1;
}
"@

Next, I created a test function and ran it with string and TestType inputs.

function Test-ParameterSets1
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Str")] [string] $StringInput,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Test")] [TestType] $TestInput
    )
    begin {
        $result = New-Object Object | Select-Object –Property @{n='FunctionName';e={$PSCmdlet.MyInvocation.InvocationName}},@{n='ParameterSetName';e={$PSCmdlet.ParameterSetName}}
    }
    process {
        $result | Add-Member -MemberType NoteProperty -Name StringInput -Value $StringInput -PassThru | Add-Member -MemberType NoteProperty -Name TestInput -Value $TestInput
    }
    end {
        $result
    }
}
'string' | Test-ParameterSets1
New-Object TestType | Test-ParameterSets1


FunctionName        ParameterSetName   StringInput TestInput
------------        ----------------   ----------- ---------
Test-ParameterSets1 __AllParameterSets string               
Test-ParameterSets1 __AllParameterSets             TestType 

This is the core of the problem. The ParameterSetName evaluates to __AllParameterSets even though as seen by the values, the parameters are set as expected. My function has many parameter sets and does lots of switching based on the parameter set to control logic flow.

Next I tried adding a parameter unique to one parameter set and as expected, the ParameterSetName was correct for the calls in which it was specified only.

function Test-ParameterSets2
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Str")] [string] $StringInput,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Test")] [TestType] $TestInput,
        [Parameter(ParameterSetName="Test")] [string] $TestName
    )
    begin {
        $result = New-Object Object | Select-Object –Property @{n='FunctionName';e={$PSCmdlet.MyInvocation.InvocationName}},@{n='ParameterSetName';e={$PSCmdlet.ParameterSetName}}
    }
    process {
        $result | Add-Member -MemberType NoteProperty -Name StringInput -Value $StringInput -PassThru | Add-Member -MemberType NoteProperty -Name TestInput -Value $TestInput
    }
    end {
        $result
    }
}
'string' | Test-ParameterSets2
New-Object TestType | Test-ParameterSets2 -TestName MyName
New-Object TestType | Test-ParameterSets2


FunctionName        ParameterSetName   StringInput TestInput
------------        ----------------   ----------- ---------
Test-ParameterSets2 __AllParameterSets string               
Test-ParameterSets2 Test                           TestType 
Test-ParameterSets2 __AllParameterSets             TestType 

Next, I tried adding a parameter that is mandatory for both parameter sets, and this time the ParameterSetName evaluated to an empty string, which was especially confusing.

function Test-ParameterSets5
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Str")] [string] $StringInput,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ParameterSetName="Test")] [TestType] $TestInput,
        [Parameter(Mandatory=$true, ParameterSetName="Str")] [Parameter(Mandatory=$true, ParameterSetName="Test")] [string] $Mandatory,
        [Parameter(ParameterSetName="Test")] [string] $TestName
    )
    begin {
        $result = New-Object Object | Select-Object –Property @{n='FunctionName';e={$PSCmdlet.MyInvocation.InvocationName}},@{n='ParameterSetName';e={$PSCmdlet.ParameterSetName}}
    }
    process {
        $result | Add-Member -MemberType NoteProperty -Name StringInput -Value $StringInput -PassThru | Add-Member -MemberType NoteProperty -Name TestInput -Value $TestInput
    }
    end {
        $result
    }
}
'string' | Test-ParameterSets5 -Mandatory mandatoryParam
New-Object TestType | Test-ParameterSets5 -Mandatory mandatoryParam -TestName MyName
New-Object TestType | Test-ParameterSets5 -Mandatory mandatoryParam 


FunctionName        ParameterSetName StringInput TestInput
------------        ---------------- ----------- ---------
Test-ParameterSets5                  string               
Test-ParameterSets5 Test                         TestType 
Test-ParameterSets5                              TestType 

It seems as though PowerShell does in fact know how to set these parameters correctly, and yet the ParameterSetName isn't evaluating properly. Is there some way to get this working? I'd like to avoid having unnecessary switches such as -String and -TestType that are unique to their own parameter sets just so that PowerShell can do its job. Thanks!


Solution

  • The problem with your code is that you read the ParameterSetName property in the begin block. When a command accepts a pipeline input, then the input object can affect the selected ParameterSetName. And if your command has multiple input objects, then each of them can result in different parameter set will be selected:

    class a { }
    class b { }
    class c { }
    function f {
        param(
            [Parameter(ParameterSetName='a', ValueFromPipeline)][a]$a,
            [Parameter(ParameterSetName='b', ValueFromPipeline)][b]$b,
            [Parameter(ParameterSetName='c', ValueFromPipeline)][c]$c
        )
        begin {
            "ParameterSetName in begin block: $($PSCmdlet.ParameterSetName)"
        }
        process {
            "ParameterSetName in process block: $($PSCmdlet.ParameterSetName)"
        }
    }
    [a]::new(), [b]::new(), [c]::new() | f
    
    # Result:
    # ParameterSetName in begin block: __AllParameterSets
    # ParameterSetName in process block: a
    # ParameterSetName in process block: b
    # ParameterSetName in process block: c
    

    Thus, if you want to know which parameter set was selected after input object was bound to your command, then you should read ParameterSetName in the process block.