A note up front:
... | ForEach-Object{ $_ }
, with a single input object, as in your case, never outputs an array (unless the input object is an array as a whole, which is atypical), even if the input was a single-element array, because of the pipeline's enumeration behavior. In fact, the pipeline itself never creates arrays on output, it streams objects one by one, and it is only when that stream is captured that an array must be created - but only for two or more output objects; see this answer for background information.
Santiago Squarzon has provided the crucial pointer: wrapping the first argument passed to [System.Linq.Enumerable]::ToLookup[TSource, TKey]()
in an [object[]]
array allows PowerShell to find the right overload of and type arguments for this method:
$productInfos = [Linq.Enumerable]::ToLookup(
@($productInfos), # wrap $productInfos in an [object[]] array with @(...)
$productIdDelegate
)
Like all LINQ methods, ToLookup
:
- operates on enumerables (loosely speaking, collections of objects),
- is a generic method, which means that it must be instantiated with the types it operates on (type arguments that bind to the method's type parameters).
PowerShell has to infer the type arguments in your call, because you're not providing the type arguments explicitly, the way you'd need to in C# (e.g., Array.Empty<int>()
)
- Note: Prior to the upcoming verion 7.3 of PowerShell (Core), PowerShell didn't even support specifying type arguments explicitly - see next section.
The signature of the ToLookup
overload you're targeting is (you can see this by executing [Linq.Enumerable]::ToLookup
, i.e. without ()
, though the following has been simplified and reformatted for readability):
# TSource and TKey are the generic type parameters.
static ... ToLookup[TSource, TKey](
IEnumerable[TSource] source,
Func[TSource,TKey] keySelector
)
Since the argument you're passing to the keySelector
parameter, $productIdDelegate
, is an instance of [Func[object,int]]
, PowerShell can infer that the argument for type parameter TSource
is [object]
, and for TKey
it is [int]
.
Since TSource
is [object]
, the argument you pass to the source
parameter must be of type IEnumerable[object]
.
Your $productInfos
value, due to being a single object, does not implement the IEnumerable[object]
interface, but an [object[]]
-typed array does.
Given that @(...)
, the array-subexpression operator, always returns an [object[]]
-typed array, it is sufficient to use it to wrap $productInfos
.
Note that had you used a specific type in lieu of [object]
, e.g. [Func[datetime, int]]
, @(...)
would not work: you would then need a type-specific cast, such as [datetime[]]
in this example.
Conversely, in your case you could have used [object[]] $productInfos
(or its equivalent, [Array] $productInfos
) as an alternative to @($productInfos)
.
See also:
This answer for an overview of using LINQ from PowerShell, which has its limitations, because PowerShell even in 7.3 won't support extension methods (yet?), which are necessary for a fluid experience.
Adding support for extension methods, and possibly also an equivalent of LINQ query syntx - is being discussed for a future version of PowerShell (Core) in GitHub issue #2226.
PowerShell 7.3+ alternative:
Versions 7.3+ have - optional - support for specifying type arguments explicitly, which is good news for two reasons:
If you apply this syntax to your call, you may omit casting your script block to [Func[object, int]]
, because this cast is implied by the explicit type parameters; using a simplified, self-contained version of your call:
# PS v7.3+ only
# Note the explicit type arguments ([object, int])
# and the use of the script block without a [Funct[object, int]] cast.
$lookup = [Linq.Enumerable]::ToLookup[object, int](
@([pscustomobject] @{ foo = 1 } , [pscustomobject] @{ foo = 2 }),
{ $args[0].foo }
)
$lookup[2] # -> [pscustomobject] @{ foo = 2 }
As in the case of inferred type arguments, if the first type argument is something other than [object]
, say [pscustomobject]
, you'd need a type-specific array cast (@(...)
alone won't be enough):
# PS v7.3+ only
# Note the [pscustomobject] type argument and the
# [pscustomobject[]] cast.
$lookup = [Linq.Enumerable]::ToLookup[pscustomobject, int](
[pscustomobject[]] ([pscustomobject] @{ foo = 1 } , [pscustomobject] @{ foo = 2 }),
{ $args[0].foo }
)