dburtsev
dburtsev

Reputation: 73

LINQ in PowerShell with dynamically specified data types

In response to my previous question, I was given a working script that is based on a known-in-advance data type literally specified as [int].

Now I would like to change the data type dynamically. Is it possible?

Upvotes: 1

Views: 201

Answers (2)

mklement0
mklement0

Reputation: 439193

The answer to your previous question uses type literals ([...]), which require that all types be specified verbatim (by their literal names).

  • Variable references are not supported in type literals; e.g. [Func[Data.DataRow, $columnType]] does not work - it causes a syntax error.

To generalize the linked answer based on dynamically determined (indirectly specified) types, two modifications are needed:

  • You must construct (instantiate) the closed generic types involved in the LINQ method call via the .MakeArrayType() and .MakeGenericType() methods.

  • You must use -as, the conditional type conversion operator to cast the input objects / the transformation script block ({ ... }) to those types.

# Create a sample data table...
[Data.DataTable] $dt = New-Object System.Data.DataTable
[Data.DataColumn] $column = New-Object System.Data.DataColumn "Id", ([int])
$dt.Columns.Add($column)
# ... and add data.
[Data.DataRow]$row = $dt.NewRow()
$row["Id"] = 1
$dt.Rows.Add($row)
$row = $dt.NewRow() 
$row["Id"] = 2
$dt.Rows.Add($row)

# 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')

# Dynamically set the name of the column to use in the projection.
$colName = 'Id'

# Dynamically set the in- and output types to use in the LINQ
# .Select() (projection) operation.
$inType = [Data.DataRow]
$outType = [int]


# Now derive the generic types required for the LINQ .Select() method
# from the types above:

# The array type to serve as the input enumerable.
# Note: As explained in the linked answer, the proper - but more cumbersome -
#       solution would be to use reflection to obtain a closed instance of
#       the generic .AsEnumerable() method.
$inArrayType = $inType.MakeArrayType()

# The type for the 'selector' argument, i.e. the delegate performing
# the transformation of each input object.
$closedFuncType = [Func`2].MakeGenericType($inType, $outType)

# Close the generic .Select() method with the given types
# and invoke it.
[int[]] $results = $selectMethod.MakeGenericMethod($inType, $outType).Invoke(
  # No instance to operate on - the method is static.
  $null,
  # The arguments for the method, as an array.
  # Note the use of the -as operator with the dynamically constructed types.
  (
    ($dt.Rows -as $inArrayType), 
    ({ $args[0].$colName } -as $closedFuncType)
  )
)

# Output the result.
$results

Taking a step back:

As shown in the linked answer, PowerShell's member-access enumeration can provide the same functionality, with greatly simplified syntax and without needing to deal with types explicitly:

# Create a sample data table...
[Data.DataTable] $dt = New-Object System.Data.DataTable
[Data.DataColumn] $column = New-Object System.Data.DataColumn "Id", ([int])
$dt.Columns.Add($column)
# ... and add data.
[Data.DataRow]$row = $dt.NewRow()
$row["Id"] = 1
$dt.Rows.Add($row)
$row = $dt.NewRow() 
$row["Id"] = 2
$dt.Rows.Add($row)

# Dynamically set the name of the column to use in the projection.
$colName = 'Id'

# Use member-access enumeration to extract the value of the $colName column
# from all rows.
$dt.$colName  # Same as: $dt.Rows.$colName

Upvotes: 2

carrvo
carrvo

Reputation: 648

You may want to back up and ask yourself why you are trying to use LINQ in PowerShell. A tip is that if it looks like C#, there is likely a better way to do it.

I assume that you are new to PowerShell so I will give you a quick heads up as to why LINQ is actually "easier" in PowerShell (technically it is not LINQ anymore but I think it looks like it is) when combined with the pipeline.

Try Get-Help *-Object on a PowerShell prompt sometime. Notice the cmdlets that show up? Select-Object, Where-Object, Group-Object, and Sort-Object do the same things as LINQ and their names match up to what you expect. Plus, there is no strongly-typed requirement strictly speaking.

$data | Where-Object -Property Greeting -Like *howdy* | Select-Object Name,Year,Greeting

Upvotes: 0

Related Questions