Reputation: 73
I am converting the application from C# to PowerShell. How can I call LINQ from PowerShell?
[Data.DataTable]$dt = New-Object System.Data.DataTable
[Data.DataColumn]$column = New-Object System.Data.DataColumn "Id", ([int])
$dt.Columns.Add($column)
# add data
[Data.DataRow]$row = $dt.NewRow() #
$row["Id"] = 1
$dt.Rows.Add($row)
$row = $dt.NewRow() #
$row["Id"] = 2
$dt.Rows.Add($row)
# LINQ in C#: int[] results = dt.AsEnumerable().Select(d => d.Field("Id")).ToArray();
[int[]]$results = [Linq.Enumerable]::Select($dt,[Func[int,int]]{ $args[0]})
# Error: Cannot find an overload for "Select" and the argument count: "2"
Write-Host $results
Upvotes: 4
Views: 2232
Reputation: 439193
Note: For general information on the limitations of using LINQ from PowerShell, see this post.
The problem is that System.Linq.Enumerable.Select()
is a generic method, and PowerShell has no way to specify types for the required type parameters, up to PowerShell 7.2.x. To get this to work, reflection would have to be used, which is quite cumbersome (see bottom section).
However, you can use a convenient PowerShell feature instead: member-access enumeration allows you to access a property of interest directly on a collection (enumerable), and PowerShell will return the property values of each element:
[int[]] $results = $dt.Id # same as: $dt.Rows.Id
$results # print -> array 1, 2
$dt.Id
is effectively the same as: $dt | ForEach-Object { $_.Id }
For the sake of completeness (it isn't worth doing for this use case), here is the reflection-based LINQ method:
Note:
# Using reflection, get the open definition of the relevant overload of the
# static [Linq.Enumerable]::Select() method.
# ("Open" means: its generic type parameters aren't yet bound, i.e. aren't
# yet instantiated with concrete types.)
$selectMethod = [Linq.Enumerable].GetMethods().Where({
$_.Name -eq 'Select' -and $_.GetParameters()[-1].ParameterType.Name -eq 'Func`2'
}, 'First')
# Close the method with the types at hand and invoke it via reflection.
[int[]] $results = $selectMethod.MakeGenericMethod([Data.DataRow], [int]).Invoke(
# No instance to operate on - the method is static.
$null,
# The arguments for the method, as an array.
(
[Data.DataRow[]] $dt.Rows,
[Func[Data.DataRow,int]] { $args[0].Id }
)
)
# Output the result.
$results
Note that the above only shows how to instantiate the generic .Select()
method.
To get an [System.Collections.Generic.IEnumerable`1[System.Data.DataRow]]
instance, a non-lazy array cast ([System.Data.DataRow[]]
) was used in lieu of using System.Linq.Enumerable.AsEnumerable()
- use of the latter too would require using the reflection-based approach.
It is evident from the above that using LINQ from PowerShell is quite cumbersome, up to at least v7.3.1 - GitHub issue #2226 proposes introducing better LINQ integration in the future.
For an - even more cumbersome - generalization of the LINQ solution that uses dynamically (indirectly) specified data types instead of data-type literals such as [int]
, see this answer to your follow-up question.
Upvotes: 6