dburtsev
dburtsev

Reputation: 73

LINQ in PowerShell

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

Answers (1)

mklement0
mklement0

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:

  • PowerShell (Core) 7.3+ now supports syntax similar to C#'s where you specify a generic method's type arguments explicitly - see the conceptual about_Calling_Generic_Methods help topic. The cumbersome reflection-based approach below is therefore only required in PowerShell (Core) 7.2- and Windows PowerShell.
# 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

Related Questions