David Haymond
David Haymond

Reputation: 75

PowerShell pipeline parameter binding order

I have an advanced function that can accept two kinds of pipeline data:

Here's my function:

function Test-PowerShell {
    [CmdletBinding(DefaultParameterSetName = "ID")]
    param (
        [Parameter(
            Mandatory = $true,
            ParameterSetName = "InputObject",
            ValueFromPipeline = $true
        )]
        [PSTypeName('MyType')]
        $InputObject,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'ID',
            ValueFromPipelineByPropertyName = $true
            )]
        [int]
        $ID
    )

    process {
        if ($InputObject) {
            $objects = $InputObject
            Write-Verbose 'InputObject binding'
        }
        else {
            $objects = Get-MyType -ID $ID
            Write-Verbose 'ID binding'
        }

        # Do something with $objects
    }
}

I can use this function like this:

$obj = [PSCustomObject]@{
    PSTypeName = 'MyType'
    ID = 5
    Name = 'Bob'
}
$obj | Test-PowerShell -Verbose

Note that this object satisfies both of the above conditions: It is a MyType, and it has an ID property. In this case, PowerShell always binds to the ID property. This isn't ideal performance-wise because the piped object is discarded and I have to re-query it using the ID. My question is this:

How do I force PowerShell to bind the pipeline to $InputObject if possible?

If I change the default parameter set to InputObject, PowerShell binds on $InputObject. I don't want this, however, because when run without parameters, I want PowerShell to prompt for an ID, not an InputObject.

Upvotes: 5

Views: 605

Answers (1)

Maximilian Burszley
Maximilian Burszley

Reputation: 19684

Simple answer: remove the Mandatory argument to the Parameter attribute on $InputObject to get the functionality you want. I don't have enough knowledge on how parameter binding works to explain why this works.

function Test-PowerShell {
    [CmdletBinding(DefaultParameterSetName = 'ID')]
    param(
        [Parameter(ParameterSetName = 'InputObject', ValueFromPipeline)]
        [PSTypeName('MyType')]
        $InputObject,

        [Parameter(ParameterSetName = 'ID', Mandatory, ValueFromPipelineByPropertyName)]
        [int]
        $ID
    )

    process {
        $PSBoundParameters
    }
}

$o = [pscustomobject]@{
    PSTypeName = 'MyType'
    ID         = 6
    Name       = 'Bob'
}


PS> $o | Test-PowerShell

Key         Value
---         -----
InputObject MyType


PS> [pscustomobject]@{ID = 6} | Test-PowerShell

Key Value
--- -----
ID      6

Thoughts and experimentation below.

Here's a workaround to your problem (defining your own type):

Add-Type -TypeDefinition @'
public class MyType
{
    public int ID { get; set; }
    public string Name { get; set; }
}
'@

And then you would tag your parameter as [MyType], creating objects like you would from [pscustomobject]:

[MyType]@{ ID = 6; Name = 'Bob' }

In hindsight, this method does not work. What you're running into is the behavior of the DefaultParameterSet. I'd suggest changing what you take as pipeline input. Is there a use-case for taking the ID as pipeline input versus a user just using Test-PowerShell -ID 5 or Test-PowerShell and being prompted for the ID?

Here's a suggestion that may work as you intend from my testing:

function Test-PowerShell {
    [CmdletBinding(DefaultParameterSetName = 'ID')]
    param(
        [Parameter(ParameterSetName = 'InputObject', Mandatory = $true, ValueFromPipeline = $true)]
        [PSTypeName('MyType')]
        $InputObject,

        [Parameter(ParameterSetName = 'ID', Mandatory = $true, ValueFromPipeline = $true)]
        [int]
        $ID
    )

    process {
        $PSBoundParameters
    }
}

To take an example from an existing built-in cmdlet, they don't use the same name or properties on an object for multiple parameters. In Get-ChildItem, both the LiteralPath and Path take pipeline input, but LiteralPath only takes it by PropertyName LiteralPath or PSPath (aliased). Path is ByValue and PropertyName, but only as Path.

Upvotes: 3

Related Questions