Property passed to Invoke-Command changes type from IDictionary to HashTable
Asked Answered
P

1

6

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?

Phore answered 20/8, 2019 at 11:46 Comment(1)
Oh wow. I always thought this is a core PS problem. But never thought a bug. mkelement0 answers that neatly and my recommendation is to Stock this question permanently. Drawing moderator attention to have this question frozen. This is one of the core PS flaws which can help all. Thank you for your effortSweeper
C
5

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.

Calctufa answered 20/8, 2019 at 11:54 Comment(3)
Excuse me if I am sounding noob, but I want to clear myself on this. What do you mean by non-generic hashtable? Are you only referring the order only?Sweeper
@RanadipDutta: Please see my update; the aspects of ordering vs. use of a generic type are unrelated. Both the unordered (@{ ... }) and the ordered ([ordered] @{ ... }) types that PowerShell supports are non-generic. (And, surprisingly, there is currently no generic ordered dictionary - see https://mcmap.net/q/143428/-no-generic-implementation-of-ordereddictionary/45375.)Calctufa
Thanks @mklement0; that's much clearer than my first tweak; nice one :)Phore

© 2022 - 2024 — McMap. All rights reserved.