Powershell: Add $PSBoundParametersObject and support for Splatting PSCustomObjects

Created on 17 May 2018  路  8Comments  路  Source: PowerShell/PowerShell

There are often scenarios where you need $PSBoundParameters but with some parameter(s) excluded, some renamed, and others possibly added to get the correct parameters needed for another function your calling.

There are many existing ways to manipulate $PSBoundParameters but they are not intuitive due to the type of $PSBoundParameters.

If we had $PSBoundParametersObject we could do things like:

$ParametersNeededToCallFunction = $PSBoundParametersObject | 
Select-Object -Property <Properties> -ExcludeProperty <ExcludedProperties> | 
Add-Member -MemberType NoteProperty -Name Path -Value "C:\Path" -PassThru |
Add-Member -MemberType AliasProperty -Name ComputerName -Value IPAddress

Adding the ability to splat PSCustomOjbects we could then do:

Invoke-Something @ParametersNeededToCallFunction

This would allow better knowledge reuse as someone who takes the time to learn Select-Object and Add-Memeber now can intuitively deal with bound parameters.

This would be better than having to learn about and deal with a lot of the quirks related to System.Management.Automation.PSBoundParametersDictionary which are not as generally applicable.

Issue-Discussion WG-Language

Most helpful comment

I'm not sure I see how this is a win. Because $PSBoundParameters is essentially just a hashtable, all the normal things you do with hashtables just work. Adding a property is simple assignment:

$PSBoundParameters.Name = "c:\Path"
$PSBoundParameters.ComputerName = $ipaddress

and removing a property is done with the Remove() method.

$PSBoundParameters.Remove("propertytoremove")

This seems simpler than messing around with Select-Object and Add-Member and will definitely be much faster. What am I missing?

All 8 comments

In case it helps provide context, I use a helper function ConvertFrom-BSBoundParameters to accomplish a similar style of coding now.

This is a neat idea, but I must say this should have a slightly shorter name!

I'm not sure I see how this is a win. Because $PSBoundParameters is essentially just a hashtable, all the normal things you do with hashtables just work. Adding a property is simple assignment:

$PSBoundParameters.Name = "c:\Path"
$PSBoundParameters.ComputerName = $ipaddress

and removing a property is done with the Remove() method.

$PSBoundParameters.Remove("propertytoremove")

This seems simpler than messing around with Select-Object and Add-Member and will definitely be much faster. What am I missing?

@BrucePay I have taken what you have said to heart and been writing code since you posted this in the style you proposed as I figured doing it this way would help me better explain the difficulties with this pattern.

Some observations:

  1. $PSBoundParameters.Remove("propertytoremove") returns True or False depending on whether the property was present so in practice you always have to remember to do something like | Out-Null or you will likely break code calling your function
  2. If we had $PSBoundParametersObject the code we write feels cleaner and more clear

    1. Below are two examples of the same function that ends up calling another function that makes a web service call that needs to have some parameters in the body of the request, some in the query string, and some to change the behavior of the function making the request (like setting authorization headers).

Using $PSBoundParameters as it is today

function New-FreshDeskTicket {
    param (
        $name,
        $requester_id,
        $email,
        $facebook_id,
        $phone,
        $twitter_id,
        $unique_external_id,
        $subject,
        #.. 17 more parameters go here but removed for brevity
        $Credential,
        $SubDomain
    )

    $InvokeAPIParameters = @{}
    $PSBoundParameters.Keys | 
    Where-Object Name -In "Credential","SubDomain" | 
    ForEach-Object { 
        $InvokeAPIParameters.Add($_, $PSBoundParameters.$_)
    }

    $QueryStringParameters = @{}
    $PSBoundParameters.Keys | 
    Where-Object Name -In "requester_id","unique_external_id" | 
    ForEach-Object { 
        $QueryStringParameters.Add($_, $PSBoundParameters.$_)
    }

    $PSBoundParameters.Remove("Credential") | Out-Null
    $PSBoundParameters.Remove("Domain") | Out-Null

    Invoke-FreshDeskAPI -Body $PSBoundParameters -QueryString $QueryStringParameters -Resource tickets -Method Post @InvokeAPIParameters
}

Using $PSBoundParametersObject

function New-FreshDeskTicket {
    param (
        $name,
        $requester_id,
        $email,
        $facebook_id,
        $phone,
        $twitter_id,
        $unique_external_id,
        $subject,
        #.. 17 more parameters go here but removed for brevity
        $Credential,
        $SubDomain
    )
    $InvokeAPIParameters = $PSBoundParametersObject | Select-Object -Property Credential,SubDomain    
    $QueryStringParameters = $PSBoundParametersObject | Select-Object -Property requester_id,unique_external_id
    $BodyParameters = $PSBoundParametersObject | Select-Object -ExcludeProperty Credential,Domain,requester_id,unique_external_id

    #When https://github.com/PowerShell/PowerShell/issues/7049 is resolved we won't need the following if statement
    if (-Not $InvokeAPIParameters) {
        $InvokeAPIParameters = [PSCustomObject]@{} 
    }

    Invoke-FreshDeskAPI -Body $BodyParameters -QueryString $QueryStringParameters -Resource tickets -Method Post @InvokeAPIParameters
}

You can pretty much do everything in your PSBoundParametersObject example if you use .GetEnumerator(). The code is just as simple as yours:

function New-FreshDeskTicket {
    param (
        $name,
        $requester_id,
        $email,
        $facebook_id,
        $phone,
        $twitter_id,
        $unique_external_id,
        $subject,
        #.. 17 more parameters go here but removed for brevity
        $Credential,
        $SubDomain
    )

    $InvokeAPIParameters = $PSBoundParameters.GetEnumerator() | Where-Object Key -In "Credential", "SubDomain"
    $QueryStringParameters = $PSBoundParameters.GetEnumerator() | Where-Object Key -In "requester_id", "unique_external_id"
    $BodyParameters = $PSBoundParameters.GetEnumerator() | Where-Object Key -NotIn @($InvokeAPIParameters.key + $QueryStringParameters.Key)

    if( -not $InvokeAPIParameters ) {
        Invoke-FreshDeskAPI -Body $BodyParameters -QueryString $QueryStringParameters -Resource tickets -Method Post @InvokeAPIParameters
    }
}

New-FreshDeskTicket -Credential "TestCred" -SubDomain "TestDomain" -requester_id "ID1" -unique_external_id "EXID1" -name "TestName" -email "TestEmail"

@dragonwolf83 I am having trouble getting an example working the way your describing things should:

PS > function Test-PSBoundGetEnumerator { param ($test1,$test2) $PSBoundParameters.GetEnumerator() | where key -in Test1,Test2 }
PS > $Variable = Test-PSBoundGetEnumerator -test1 value1 -test2 value2                                           
PS > $Variable                                                                                                   

Key   Value
---   -----
test1 value1
test2 value2

PS > $Variable | gm                                                                                              


   TypeName: System.Collections.Generic.KeyValuePair`2[[System.String, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, 
PublicKeyToken=7cec85d7bea7798e]]

Name        MemberType Definition
----        ---------- ----------
Deconstruct Method     void Deconstruct([ref] string key, [ref] System.Object value)
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
ToString    Method     string ToString()
Key         Property   string Key {get;}
Value       Property   System.Object Value {get;}

   TypeName: System.Object[]

Name           MemberType            Definition
----           ----------            ----------
Add            Method                int IList.Add(System.Object value)
Address        Method                System.Object&, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7c...
Clear          Method                void IList.Clear()
Clone          Method                System.Object Clone(), System.Object ICloneable.Clone()
CompareTo      Method                int IStructuralComparable.CompareTo(System.Object other, System.Collections.IComparer compa...
Contains       Method                bool IList.Contains(System.Object value)
CopyTo         Method                void CopyTo(array array, int index), void CopyTo(array array, long index), void ICollection...
Equals         Method                bool Equals(System.Object obj), bool IStructuralEquatable.Equals(System.Object other, Syste...
Get            Method                System.Object Get(int )
GetEnumerator  Method                System.Collections.IEnumerator GetEnumerator(), System.Collections.IEnumerator IEnumerable....
GetHashCode    Method                int GetHashCode(), int IStructuralEquatable.GetHashCode(System.Collections.IEqualityCompare...
GetLength      Method                int GetLength(int dimension)
GetLongLength  Method                long GetLongLength(int dimension)
GetLowerBound  Method                int GetLowerBound(int dimension)
GetType        Method                type GetType()
GetUpperBound  Method                int GetUpperBound(int dimension)
GetValue       Method                System.Object GetValue(Params int[] indices), System.Object GetValue(int index), System.Obj...
IndexOf        Method                int IList.IndexOf(System.Object value)
Initialize     Method                void Initialize()
Insert         Method                void IList.Insert(int index, System.Object value)
Remove         Method                void IList.Remove(System.Object value)
RemoveAt       Method                void IList.RemoveAt(int index)
Set            Method                void Set(int , System.Object )
SetValue       Method                void SetValue(System.Object value, int index), void SetValue(System.Object value, int index...
ToString       Method                string ToString()
Item           ParameterizedProperty System.Object IList.Item(int index) {get;set;}
Count          Property              int Count {get;}
IsFixedSize    Property              bool IsFixedSize {get;}
IsReadOnly     Property              bool IsReadOnly {get;}
IsSynchronized Property              bool IsSynchronized {get;}
Length         Property              int Length {get;}
LongLength     Property              long LongLength {get;}
Rank           Property              int Rank {get;}
SyncRoot       Property              System.Object SyncRoot {get;}

PS > Test-PSBoundGetEnumerator @Variable                                                                         

Key   Value
---   -----
test1 [test1, value1]
test2 [test2, value2]

PS > $VariableHash = @{test1 = "value1"; test2 = "value2"}                                                                                                                                      
PS > Test-PSBoundGetEnumerator @VariableHash                                                                     

Key   Value
---   -----
test2 value2
test1 value1

It doesn't appear that splatting a System.Object[] of System.Collections.Generic.KeyValuePair``2[[System.String, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Object, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] behaves the same way as splatting a System.Collections.Hashtable.

Your right, it is including the key with the value as the value. In theory, the ToDictionary LINQ Extension Method should convert it back to a workable state but don't know if that is working yet in 6.0+.

Maybe Splatting just needs to be updated to handle passing the KeyValuePair type directly and handle that weirdness for us.

@dragonwolf83 Just for information's sake, the above test was with PowerShell version 6.1.0-preview.2.

I think your intuition about how this should work not aligning with how it actually works is further evidence that this does not align with the principle of least astonishment and that things would be better/less surprising if we had an object to work with and could splat objects instead of dealing with the idiosyncrasies of dictionaries/hashtables for this particular purpose.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

concentrateddon picture concentrateddon  路  3Comments

aragula12 picture aragula12  路  3Comments

pcgeek86 picture pcgeek86  路  3Comments

andschwa picture andschwa  路  3Comments

garegin16 picture garegin16  路  3Comments