1

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?

mklement0
  • 382,024
  • 64
  • 607
  • 775
dburtsev
  • 63
  • 5

2 Answers2

1

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
mklement0
  • 382,024
  • 64
  • 607
  • 775
0

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
carrvo
  • 511
  • 5
  • 11
  • 1
    > Select-Object, Where-Object, Group-Object, and Sort-Object do the same things as LINQ Speed is not the same on big tables – dburtsev Jan 10 '22 at 18:15
  • If you are looking for speed, no interpreted language is going to match up to a compiled language so you are using the wrong medium. If you want the speed but still want advantages of an interpreted language, then I suggest writing your LINQ in a binary cmdlet and handing your collection in. If you still insist on doing it entirely in the interpreted language, then your code will smell of bad design and will only cause future issues for you. Please re-read my very first sentence ("...back up and ask yourself..."). – carrvo Jan 12 '22 at 05:47