Property passed to Invoke-Command changes type from IDictionary to HashTable

599 Views Asked by At

I've been getting an error running Invoke-Command where the script block takes a parameter of type dictionary:

Cannot process argument transformation on parameter 'dictionary'. Cannot convert the "System.Collections.Hashtable" value of type "System.Collections.Hashtable" to type "System.Collections.Generic.IDictionary`2[System.String,System.String]". At line:7 char:1 + Invoke-Command -ComputerName . -ArgumentList $dictionary -ScriptBlock ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [], ParameterBindin...mationException + FullyQualifiedErrorId : ParameterArgumentTransformationError + PSComputerName : localhost

After a lot of digging I was able to reduce the script to the the MVP below to show the root of this issue:

[System.Collections.Generic.IDictionary[string, string]]$dictionary = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, string]' 
$dictionary.Add('one', 'hello')
$dictionary.Add('two', 'world')
Write-Verbose "Main Script $($dictionary.GetType().FullName)" -Verbose #outputs: VERBOSE: Before System.Collections.Generic.Dictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Invoke-Command -ComputerName . -ArgumentList $dictionary -ScriptBlock {
    Param (
        #[System.Collections.Generic.IDictionary[string, string]] #if I leave this in I get a conversion error
        $dictionary
    )
    Write-Verbose "Function before $($dictionary.GetType().FullName)" -Verbose #outputs: VERBOSE: After System.Collections.Hashtable
    function Poc {} #this line seems to cause the `$dictionary` to become a HashTable
    Write-Verbose "Function after $($dictionary.GetType().FullName)" -Verbose #outputs: VERBOSE: After System.Collections.Hashtable

}

It seems that if the script block for Invoke-Command includes any inline functions then the parameter is automatically converted to a HashTable; whilst if the script block doesn't contain any nested function definitions the parameter is left as a System.Collections.Generic.IDictionary[string, string].

Am I misusing this feature / is there a common workaround? Or is this a just a bug in PowerShell?

1

There are 1 best solutions below

3
On BEST ANSWER

You're seeing a by-design limitation of the XML-based serialization infrastructure that underlies PowerShell remoting (which is what Invoke-Command -ComputerName is based on):

  • Objects of types that implement the IDictionary interface are deserialized as only one of two - non-generic - types (even though the original, full type name is recorded in the serialization data), namely as either:

    • In PowerShell 7.3.0 and above only (see below), [ordered] hashtables (System.Collections.Specialized.OrderedDictionary) if and only if that precise type was used as input.[2]

    • [hashtable] (System.Collections.Hashtable) for all other dictionary types, including generic types such as [System.Collections.Generic.IDictionary[string, string]] in your case; both the keys and values of this type [object]-typed.

Such potential loss of type fidelity is inherent in PowerShell's remoting, because only a limited set of well-known types are faithfully deserialized, because the idea is to make remoting work across both different PowerShell versions and nowadays between the two PowerShell editions. See this answer for a systematic overview of PowerShell's serialization.

For basic usage of dictionary types, a loss of type fidelity shouldn't matter, however: Thanks to the IDictionary interface, enumeration, entry access and getting the entry count all work the same.

However, in PowerShell 7.2.x and below, including in Windows PowerShell, [ordered] hashtables were incorrectly deserialized as unordered [hashtable]s, which notably lost the original ordering of their entries and prevented entry access by positional index on the deserialized object.