Contents
My recent article A Visual Lexicon of LINQ and accompanying wallchart provided a new, easier-to-use reference for LINQ operators when working in C#. But since one of PowerShell’s mantras is “anything you can do in C# can be done in PowerShell, too!” it is only fitting to provide a LINQ reference for PowerShell as well. This reference again itemizes every LINQ operator (and in the same order as the original article) and gives some emphasis to potential performance gains from using LINQ where possible when doing operations on large data sets.
PowerShell is an interpreted scripting language, and so is slow at using an iterative loop of any great size. By ‘slow’, it can be the difference between ten minutes in PowerShell as opposed to six seconds in C#! If a loop iterates more than sixteen times, the code of the loop is compiled dynamically as .NET code and the dynamic method is then invoked inside the loop. Also, because any scripting language presents a potential security risk, .NET must run a security check on the stack, which slows the loop down. LINQ has many aggregate and filtering functions that can be used instead of the PowerShell equivalent, and they are very likely to give you an appreciable performance improvement—but at the cost of a tedious overhead in writing the script, as detailed next.
General Notes
Why do I mention a tedious overhead of use? In PowerShell, it is straightforward to access conventional C# methods—for example,"abc".Replace("a", "A")
works just fine in PowerShell. However, most LINQ operators are static extension methods and many of those require delegate arguments, so invoking them from PowerShell is more involved. (You could say that technically those arguments are anonymous functions or even lambda expressions but I’ve chosen to use the term delegate here as it is shorter and practically equivalent in this context.) For this reason, I’ve given, in this article working examples of the use of every LINQ operator, along with the C# equivalent and the conventional PowerShell way of doing the same thing. This should enable you to judge for yourself.
Format of Entries
For each entry you will find:
- A useful yet simple (or simple-ish) example of how to use the LINQ operator in C# as a reference point.
- A translation of that example into PowerShell, i.e. calling the actual LINQ operator from PowerShell.
- An alternate version of code to perform the same operation using native PowerShell (i.e. doing it “the PowerShell way”).
Deferred vs. Immediate Execution
Unlike conventional functions and methods, many LINQ methods use deferred execution instead of immediate execution. That is, an expression with some LINQ operators does not actually calculate and yield a result until it is actually needed. (The wallchart shows you at a glance which operators this applies to.) Rather, those operators return a query (essentially an IEnumerable<T>
). Unless you are doing further LINQ operations on it, you need to convert it with a LINQ operator that materializes the result to work with it further in PowerShell; ToArray
or ToList
are the most common LINQ methods for doing that.
Calling a LINQ Operator
LINQ operators are static extension methods. In PowerShell plain static method calls require this syntax:
1 |
[ClassName]::MethodName(arguments...) |
Extension methods in C#, as you’ll recall, look like any other method available from an object, e.g.
1 |
ObjectInstance.MethodName(arguments...) |
But in PowerShell, the ObjectInstance
moves into the first argument position of the static call:
1 |
[ClassName]::MethodName(ObjectInstance, arguments...) |
The only other piece you need to know is the ClassName
to use, which will always be Linq.Enumerable
. Thus, numbers.Sum()
in C# becomes [Linq.Enumerable]::Sum($numbers)
in PowerShell. However, that will only work if the $numbers
array has the correct type and by default it does not. The next section explains further.
Explicit Argument Typing
PowerShell is a dynamically typed language rather than a statically typed language. And it is not strongly-typed (because a variable can change its type at runtime). But PowerShell does support explicit typing of variables if you choose to use it—and for LINQ calls you have no choice in the matter! Consider the rather innocuous LINQ call in C#:
1 |
Enumerable.Range(0, 100).Sum() |
This would seem to translate to PowerShell readily as:
1 |
[Linq.Enumerable]::Sum(0..100) |
Unfortunately, the result of that expression is an error:
Cannot find an overload for “Sum” and the argument count: “1”.
That error makes you think the argument count is wrong but in fact it is the type of the argument that is incorrect, as we can demonstrate.
1 2 |
PS> (0..100).GetType().Name Object[] |
The Sum
method does not take an Object
array, but it has overloads for a variety of numerical types. Thus you need to explicitly type the array; here is one way to do that:
1 2 |
PS> [Linq.Enumerable]::Sum([int[]]@(0..100)) 5050 |
Creating and Passing Delegates
Passing simple arguments to LINQ, as just shown, require explicit typing. This is even more important when using a LINQ operator with an argument that is a delegate (or anonymous function). Consider the ubiquitous Where
operator. Let’s say you have an array of date objects and you wish to filter those. In C# you might write:
1 |
var result = dates.Where(d => d.Year > 2016); |
In PowerShell, you write the equivalent delegate like this:
1 |
PS> [Func[DateTime,bool]] $delegate = { param($d); return $d.Year -gt 2016 } |
Then can make the call:
1 |
PS> [Linq.Enumerable]::Where($dates, $delegate) |
Or if you prefer to write a single expression, you still need explicit typing (I’ve added line breaks just for clarity):
1 2 3 4 |
[Linq.Enumerable]::Where( $dates, [Func[DateTime,bool]] { param($d); return $d.Year -gt 2016 } ) |
Generic LINQ Operators
Just 3 LINQ operators are generic: Cast
, OfType
, and Empty
. As it turns out, calling a generic, static, extension LINQ method requires a rather convoluted incantation in PowerShell. Consider this standard LINQ call in C# to filter a list to just strings:
1 2 |
var stuff = new object[] { "12345", 12, "def" }; var stringsInList = stuff.OfType<string>(); |
The first step for PowerShell conversion, oddly enough is to rewrite that—still in C#— so that it can be translated to PowerShell (yes, I know this is ugly!):
1 2 3 4 5 |
var stuff = new object[] { "12345", 12, "def" }; var ofTypeForString = typeof(System.Linq.Enumerable) .GetMethod("OfType") .MakeGenericMethod(typeof(string)); var stringsInList = ofTypeForString.Invoke(null, new[] { stuff }); |
But now, you can get there from here (i.e. you can write it in PowerShell):
PS> $stuff = @("12345", 12, "def") PS> $stringType = "".GetType() # set to your target type PS> $ofTypeForString = [Linq.Enumerable].GetMethod("OfType").MakeGenericMethod($stringType) # The last comma below wraps the array arg $stuff within another array PS> $ofTypeForString.Invoke($null, (,$stuff))
LINQ Chaining
One of the strong benefits of LINQ is its chaining capability. Here’s a simple example in C#:
1 |
var result = dates.OrderBy(d => d.Year).ThenBy(d => d.Month); |
While you can still do this in PowerShell, it unfortunately does not allow that wonderfully smooth fluent syntax because of the way you have to write calls to extension methods explained above. You are limited to only conventional method calls:
1 2 3 |
[Linq.Enumerable]::ThenBy( [Linq.Enumerable]::OrderBy($dates, $yearDelegate), $monthDelegate) |
Performance
Why bother with all this tedious overhead mentioned so far? In a word, performance! PowerShell was never designed to compete in terms of speed with the likes of C#. And most of the time that is perfectly fine. With small data structures or simple programs you may never even notice performance that is sub-optimal. But PowerShell is a first-class language, so you could write elaborate code dealing with huge data structures. That is when performance should definitely be kept in mind.
First, here’s a handy little function showing one way to measure performance. Note that if you just want the performance numbers, the built-in Measure-Command
cmdlet would work fine, but I wanted to get two outputs: the performance and the actual result of the evaluation (the reason for this is explained just a bit further down).
1 2 3 4 5 6 7 8 |
function Measure-Expression($codeAsString) { $stopwatch = [system.diagnostics.stopwatch]::startNew() $result = Invoke-Expression $codeAsString $stopwatch.Stop() 'result={0}, milliseconds={1,10}' ` -f $result, $stopwatch.Elapsed.TotalMilliseconds.ToString("0.0") } |
To use this, simply wrap the expression you wish to evaluate in a string and pass it to the function. This code compares the LINQ Sum
method with three other ways to do the same thing in native PowerShell:
1 2 3 4 5 |
[int[]] $numbers = 1..10000 Measure-Expression '[Linq.Enumerable]::Sum($numbers)' Measure-Expression '($numbers | Measure-Object -sum).Sum' Measure-Expression '$numbers | ForEach { $sum += $_ } -Begin { $sum = 0 } -End { $sum }’ Measure-Expression '$sum = 0; foreach ($n in $numbers) { $sum += $n }; $sum' |
It has long been known that the foreach
operator is much more snappy than piping data to the ForEach-Object
cmdlet (see e.g. Thomas Lee’s Performance with PowerShell ). Also, PowerShell Engine Improvements reveals that WMF 5.1 (released January 2017) has made substantial improvements in the core PowerShell engine; of specific interest is that piping to the ForEach-Object
cmdlet is twice as fast as it used to be (but still foreach
prevails). Also, be sure to take a look at the short list of PowerShell scripting performance considerations from Microsoft.
Those considerations are important, yes, but take a look at the actual results here: LINQ outperforms even the best native PowerShell by an order of magnitude!
Basic Command |
Time (milliseconds) |
[Linq.Enumerable]::Sum($numbers) |
0.4 |
($numbers | Measure-Object -sum).Sum |
79.8 |
$numbers | ForEach { $sum += $_ } -Begin { $sum = 0 } -End { $sum } |
156.0 |
$sum = 0; foreach ($n in $numbers) { $sum += $n }; $sum |
29.5 |
One minor detail to note on the above figures: The first time you invoke a LINQ expression in a PowerShell session there is some overhead loading the assembly. That slows down the performance by an order of magnitude—so that it is only as fast as the fastest native PowerShell call (the foreach
loop). But for every invocation thereafter in the same session, you get the much faster execution times.
Note that I am not claiming that every LINQ operator will show this dramatic performance difference. It seems to hold true for the important aggregate operators like Sum
(Count
, Average
, etc.) but I have not performance-tested the whole gamut of other LINQ operators.
One final consideration when doing performance studies of LINQ operators in PowerShell: you need to take into account whether the operator uses deferred or immediate execution. Sum
, used in the above example, uses immediate execution. That is, it produces an output that can be consumed by the rest of your PowerShell code. So if you are comparing a LINQ expression with a non-LINQ one, make sure you’re comparing like expressions. That is, if you use an operator with deferred execution, you need to include something like a ToArray
call to realize the results as part of your measurement. That’s why I wrote the Measure-Expression
function above to report not just the execution time but also the result of the expression; you will see right away if you’re doing a valid “apples-to-apples” comparison.
LINQ to PowerShell Lexicon
Aggregate
Count
Returns the number of elements in a sequence. When the result is expected to be greater than Int32.MaxValue()
, use LongCount
. If you specify an optional condition, Count
returns the number of elements in a sequence that satisfies that condition.
LINQ in C#
1 2 3 |
int[] numbers = { 3, 1, 4, 1, 5, 9, 2 }; var countAll = numbers.Count(); var countSome = numbers.Count(n => n > 2); |
LINQ in PowerShell
1 2 3 4 5 |
PS> [int[]] $numbers = @(3, 1, 4, 1, 5, 9, 2) PS> [Linq.Enumerable]::Count($numbers) 7 PS> [Linq.Enumerable]::Count($numbers, [Func[int,bool]] { $args[0] -gt 2 }) 4 |
Native PowerShell
1 2 3 4 |
PS> $numbers.Count 7 PS> $numbers.Count | Where { $_ -gt 2 } 4 |
or
1 2 3 4 |
PS> ($numbers | Measure-Object).Count 7 PS> ($numbers | Where { $_ -gt 2 } | Measure-Object).Count 4 |
LongCount
Returns as an Int64 the number of elements in a sequence. Use LongCount
rather than Count
when the result is expected to be greater than Int32.MaxValue()
. LongCount
, like Count
, allows an optional condition.
Works identically to Count
.
Sum
Computes the sum of a sequence of values. If you specify an optional transformation function, Sum
computes the sum of a sequence of values after applying that transformation on each element.
LINQ in C#
1 2 3 4 5 |
int[] numbers = Enumerable.Range(1, 10000); Func<int,int> func = n => n % 3 ? n : -n; var sumOriginal = numbers.Sum(); var sumConverted = numbers.Sum(func)); |
LINQ in PowerShell
1 2 3 4 5 |
PS> [int[]] $numbers = 1..10000 PS> [Func[int,int]] $delegate = { param ($n); if ($n % 3) { $n } else { -$n } } PS> [Linq.Enumerable]::Sum($numbers) PS> [Linq.Enumerable]::Sum($numbers, $delegate) |
Native PowerShell
PS> [int[]] $numbers = 1..10000 PS> function func($n) { if ($n % 3) { $n } else { -$n } } # basic command PS> ($numbers | Measure-Object -Sum).Sum PS> $numbers | ForEach { $sum += $_ } -Begin { $sum = 0 } -End { $sum } PS> $sum = 0; foreach ($n in $numbers) { $sum += $n }; $sum # command with transformation PS> ($numbers | ForEach { func $_ } | Measure-Object -Sum).Sum PS> $sum = 0; foreach ($n in $numbers) { $sum += func $n }; $sum
Average
Computes the average of a sequence of values. If you specify an optional transformation function, Average
computes the average of a sequence of values after applying that transformation on each element.
LINQ in C#
1 2 3 4 5 |
int[] numbers = Enumerable.Range(1, 10000); Func<int,int> func = n => n % 5 ? 100*n : n; var averageOriginal = numbers.Average(); var averageConverted = numbers.Average(func)); |
LINQ in PowerShell
1 2 3 4 5 |
PS> [int[]] $numbers = 1..10000 PS> [Func[int,int]] $delegate = { param ($n); if ($n % 5) { 100 * $n } else { $n } } PS> [Linq.Enumerable]::Average($numbers) PS> [Linq.Enumerable]::Average($numbers, $delegate) |
Native PowerShell
PS> [int[]] $numbers = 1..10000 PS> function func($n) { if ($n % 5) { 100 * $n } else { $n } } # basic command PS> ($numbers | Measure-Object -Average).Average PS> $numbers | ForEach { $sum += $_ } -Begin { $sum = 0 } -End { $sum / $numbers.Length } PS> $sum = 0; foreach ($n in $numbers) { $sum += $n }; $sum / $numbers.Length # command with transformation PS> ($numbers | ForEach { func $_ } | Measure-Object -Average).Average PS> $sum = 0; foreach ($n in $numbers) { $sum += func $n }; $sum / $numbers.Length
Max
Returns the maximum value in a sequence. If you specify an optional transformation function, Max
returns the maximum value in a sequence after applying that transformation on each element.
LINQ in C#
1 2 3 4 5 |
int[] numbers = Enumerable.Range(1, 10000); Func<int,int> func = n => n % 5 ? 100*n : n; var maximumOriginal = numbers.Max(); var maximumConverted = numbers.Max(func)); |
LINQ in PowerShell
1 2 3 4 |
PS> [int[]] $numbers = 1..10000 PS> [Func[int,int]] $delegate = { param ($n); if ($n % 5) { 100 * $n } else { $n } } PS> [Linq.Enumerable]::Max($numbers) |
Native PowerShell
PS> [int[]] $numbers = 1..10000 PS> function func($n) { if ($n % 5) { 100 * $n } else { $n } } # basic command PS> ($numbers | Measure-Object -Maximum).Maximum PS> $numbers | ForEach {if ($_ -gt $max) {$max=$_}} -Begin {$max=[int]::MinValue} -End {$max} PS> $max=[int]::MinValue; foreach ($n in $numbers) { if ($n -gt $max) {$max=$n}}; $max # command with transformation PS> ($numbers | ForEach { func $_ } | Measure-Object -Maximum).Maximum PS> $max=[int]::MinValue; foreach ($n in $numbers) {$n=func $n; if ($n -gt $max) {$max=$n}}; $max
Min
Returns the minimum value in a sequence. If you specify an optional transformation function, Min
returns the minimum value in a sequence after applying that transformation on each element.
LINQ in C#
1 2 3 4 5 |
int[] numbers = Enumerable.Range(1, 10000); Func<int,int> func = n => n % 5 ? 100*n : n; var maximumOriginal = numbers.Min(); var maximumConverted = numbers.Min(func)); |
LINQ in PowerShell
1 2 3 4 5 |
PS> [int[]] $numbers = 1..10000 PS> [Func[int,int]] $delegate = { param ($n); if ($n % 5) { -100 * $n } else { $n } } PS> [Linq.Enumerable]::Min($numbers) PS> [Linq.Enumerable]::Min($numbers, $delegate) |
Native PowerShell
PS> [int[]] $numbers = 1..10000 PS> function func($n) { if ($n % 5) { -100 * $n } else { $n } } # basic command PS> ($numbers | Measure-Object -Minimum).Minimum PS> $numbers | ForEach {if ($_ -lt $min) {$min=$_}} -Begin {$min=[int]::MaxValue} -End {$min} PS> $min=[int]::MaxValue; foreach ($n in $numbers) { if ($n -lt $min) {$min=$n}}; $min # command with transformation PS> ($numbers | ForEach { func $_ } | Measure-Object -Minimum).Minimum PS> $min=[int]::MaxValue; foreach ($n in $numbers) {$n=func $n; if ($n -lt $min) {$min=$n}}; $min
Aggregate
Applies an accumulator function over a sequence. You specify a two-argument function to perform an arbitrary aggregation function of your choice. The first parameter is the accumulated results so far, which is initialized to the default value for the element type, and the second parameter is the sequence element.
LINQ in C#
1 2 3 4 |
int[] numbers = { 5, 4, 1, 3, 9 }; var result = numbers.Aggregate( (resultSoFar, next) => resultSoFar * next); |
LINQ in PowerShell
1 2 3 4 |
PS> [int[]]$numbers = @( 5, 4, 1, 3, 9 ) PS> [Func[int,int,int]] $delegate = { param($resultSoFar, $next); $resultSoFar * $next } PS> [Linq.Enumerable]::Aggregate($numbers, $delegate) 540 |
Native PowerShell
1 2 |
PS> $numbers | ForEach-Object { $result = 1 } { $result *= $_ } { $result } 540 |
If you specify an initial seed, Aggregate
applies an accumulator function over a sequence with that initial seed value. While the seed could just be (depending on your needs) some constant integer or constant string, it could also create an object that your accumulator function will call methods against, as shown next. Here a StringBuilder
is created that is used in each step of the aggregation.
LINQ in C#
1 2 3 4 5 6 |
var words = new[] { "w1", "w2", "w3", "w4" }; var text = words.Aggregate( new StringBuilder(), (a, b) => a.Append(b + '.') ); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 9 |
PS> [string[]]$words = @("w1", "w2", "w3", "w4") PS> $delegate = [Func[System.Text.StringBuilder, string, System.Text.StringBuilder]] { param($builder, $s); $builder.Append($s + '.')} PS> [Linq.Enumerable]::Aggregate( $words, [System.Text.StringBuilder]::new(), $delegate ).ToString() w1.w2.w3.w4. |
Conversion
Cast
Casts the elements of an IEnumerable
to the specified type, effectively converting IEnumerable
to IEnumerable<T>
, which then makes the sequence amenable to further LINQ operations. Alternately, it can be used like OfType
which filters based on a specified type. However, whereas OfType
ignores members that are not convertible to the target type, Cast
throws an exception when it encounters such members, as the examples here reveal.
LINQ in C#
1 2 |
var stuff = new object[] { "12345", 12, "def" }; var stringsInList = stuff.Cast(); // throws exception because of the int in the array |
LINQ in PowerShell
As discussed in the introduction, generic calls need to be rewritten before being translated:
1 2 3 4 5 |
var stuff = new object[] { "12345", 12, "def" }; var castForString = typeof(System.Linq.Enumerable) .GetMethod("Cast") .MakeGenericMethod(typeof(string)); var stringsInList = castForString.Invoke(null, new[] { stuff }); // throws exception due to int |
And that translates to PowerShell as:
PS> $stuff = @("12345", 12, "def") PS> $stringType = "".GetType() # set to your target type PS> $castForString = [Linq.Enumerable].GetMethod("Cast").MakeGenericMethod($stringType) # The last comma below wraps the array arg $stuff within another array PS> $castForString.Invoke($null, (,$stuff)) Unable to cast object of type 'System.Int32' to type 'System.String'
Native PowerShell
PS> $stuff = @("12345", 12, "def")
PS> $stuff | ForEach-Object {
if ($_ -is [string]) { $_ } else { throw "$($_): incompatible type" }
}
12: incompatible type
OfType
Filters the elements of an IEnumerable
based on a specified type.
LINQ in C#
1 2 |
var stuff = new object[] { "12345", 12, "def" }; var stringsInList = stuff.OfType<string>(); |
LINQ in PowerShell
As discussed in the introduction, generic calls need to be rewritten before being translated:
1 2 3 4 5 |
var stuff = new object[] { "12345", 12, "def" }; var ofTypeForString = typeof(System.Linq.Enumerable) .GetMethod("OfType") .MakeGenericMethod(typeof(string)); var stringsInList = ofTypeForString.Invoke(null, new[] { stuff }); |
And that translates to PowerShell as:
PS> $stuff = @("12345", 12, "def") PS> $stringType = "".GetType() # set to your target type PS> $ofTypeForString = [Linq.Enumerable].GetMethod("OfType").MakeGenericMethod($stringType) # The last comma below wraps the array arg $stuff within another array PS> $ofTypeForString.Invoke($null, (,$stuff)) 12345 def
Native PowerShell
1 |
$stuff | Where-Object { $_ -is [string] } |
ToArray
Creates an array from an IEnumerable<T>
.
LINQ in C#
1 2 |
var query = Enumerable.Range(0, 4); var array = query.ToArray(); |
LINQ in PowerShell
1 2 |
$query = [Linq.Enumerable]::Range(0,4) $array = [Linq.Enumerable]::ToArray($query) |
If you have a deferred LINQ query, you can view its result set in PowerShell as if it were seemingly an array or list but you cannot access its member elements until you actually complete the LINQ invocation. An example shows this simply. Here, $query
looks like an array or list when evaluated in line (2), but line (3) shows it is not:
(1)> $query = [Linq.Enumerable]::Range(0,4)
(2)> $query
0
1
2
3
(3)> $query[3]
Unable to index into an object of type System.Linq.Enumerable+<RangeIterator>d__110
Rather, you need to use ToArray
to realize the results of the query:
1 2 3 4 5 6 7 8 |
(1)> $query = [Linq.Enumerable]::Range(0,4) (2)> $array = [Linq.Enumerable]::ToArray($query) (3)> $array.GetType.Name Int32[] (4) $array[3] 3 (5) $array[3].GetType().Name Int32 |
ToList
Creates a List<T>
from an IEnumerable<T>
.
LINQ in C#
1 2 |
var query = Enumerable.Range(0, 4); var list = query.ToList(); |
LINQ in PowerShell
1 2 |
$query = [Linq.Enumerable]::Range(0,4) $list = [Linq.Enumerable]::ToList($query) |
If you have a deferred LINQ query, you can view its result set in PowerShell as if it were seemingly an array or list but you cannot access its member elements until you actually complete the LINQ invocation. An example shows this simply. Here, $query
looks like an array or list when evaluated in line (2), but line (3) shows it is not:
(1)> $query = [Linq.Enumerable]::Range(0,4)
(2)> $query
0
1
2
3
(3)> $query[3]
Unable to index into an object of type System.Linq.Enumerable+<RangeIterator>d__110
Rather, you need to use ToList
to realize the results of the query:
1 2 3 4 5 6 7 8 |
(1)> $query = [Linq.Enumerable]::Range(0,4) (2)> $list = [Linq.Enumerable]::ToList($query) (3)> $list.GetType.Name List`1 (4) $list[3] 3 (5) $list[3].GetType().Name Int32 |
ToDictionary
Creates a Dictionary<TKey, TValue>
from an IEnumerable<T>
according to a specified key selector function (person => person.SSN
in this example).
A Dictionary
is a one-to-one map, and is editable after creation. Querying on a non-existent key throws an exception. Contrast this with ToLookup
.
(C# adapted from LINQ: Quickly Create Dictionaries with ToDictionary.)
LINQ in C#
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Person { public string SSN { get; set; } public string FirstName { get; set; } public string Surname { get; set; } } var peopleList = new List<Person> { new Person {SSN = "1001", FirstName = "Bob", Surname = "Smith"}, new Person {SSN = "2002", FirstName = "Jane", Surname = "Doe"}, new Person {SSN = "3003", FirstName = "Fester", Surname = "Adams"} }; Var peopleDict = peopleList.ToDictionary(person => person.SSN); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Person { [string] $SSN; [string] $FirstName; [string] $Surname; Person([string]$SSN, [string]$firstname, [string]$surname) { $this.Surname = $surname $this.FirstName = $firstname $this.SSN = $ssn } } [Person[]]$peopleList = @( [Person]::new("1001", "Bob", "Smith"), [Person]::new("2002", "Jane", "Doe"), [Person]::new("3003", "Fester", "Adams") ) PS> $keyDelegate = [Func[Person,string]] { $args[0].SSN } PS> $dict = [Linq.Enumerable]::ToDictionary($peopleList, $keyDelegate) PS> $dict Key Value --- ----- 1001 Person 2002 Person 3003 Person PS> $dict['1001'] SSN FirstName Surname --- --------- ------- 1001 Bob Smith |
The value of the dictionary entry (TValue
) is just the current input element from the sequence unless you specify the optional element selector function, in which case the value is computed with that function. The next example creates a composite full name for the value.
# Use the same setup as above, then just...
PS> $fullNameDelegate = [Func[Person,string]] { '{0} {1}' -f $args[0].FirstName, $args[0].Surname }
PS> $dict = [Linq.Enumerable]::ToDictionary($peopleList, $keyDelegate, $fullNameDelegate)
PS> $dict
Key Value
--- -----
1001 Bob Smith
2002 Jane Doe
3003 Fester Adams
PS> $dict['1001']
Bob Smith
Native PowerShell
PowerShell uses hash tables natively. They work very much like .NET dictionaries but you have a Name
and Value
instead of a Key
and Value
:
# Use the same setup as above, then just...
PS> $peopleList | foreach { $hash = @{} } { $hash[$_.SSN] = $_ }
PS> $hash
Name Value
---- -----
2002 Person
3003 Person
1001 Person
[508]: $hash['1001']
SSN FirstName Surname
--- --------- -------
1001 Bob Smith
PS> $peopleList | foreach { $hash = @{} }
{ $hash[$_.ssn] = ('{0} {1}' -f $_.FirstName, $_.Surname) }
PS> $hash
Name Value
---- -----
2002 Jane Doe
3003 Fester Adams
1001 Bob Smith
PS> $hash['1001']
Bob Smith
ToLookup
Creates a Lookup<TKey, TElement>
from an IEnumerable<T>
according to a specified key selector function (c =>
c.Length
in this example). If the optional element selector function is also provided, the value of the lookup element (TElement
) is computed with that function (not used in this example; see ToDictionary
for a sample usage).
A Lookup
is a one-to-many map that is not mutable after creation. Querying on a non-existent key returns an empty sequence. Contrast this with ToDictionary
. (Note that Lookup<TKey,TValue>
is roughly comparable to a Dictionary<TKey,IEnumerable<TValue>>
. Thanks to Mark Gravell for this tip on Stack Overflow.)
LINQ in C#
1 2 3 4 5 6 7 8 9 |
var colors = new List { "green", "blue", "red", "yellow", "orange", "black" }; var result = colors.ToLookup(c => c.Length); |
LINQ in PowerShell
PS> [string[]]$colors = @( "green", "blue", "red", "yellow", "orange", "black" ) PS> $lengthDelegate = [Func[string,int]] { $args[0].Length } PS> $lookup = [Linq.Enumerable]::ToLookup($colors, $lengthDelegate) # Keys in the Lookup are string lengths per the given delegate PS> $lookup[6] yellow orange # But the result is not a list or array yet! PS> $lookup[6].GetType().Name Grouping PS> $6LetterColors = [Linq.Enumerable]::ToArray($lookup[6]) PS> $6LetterColors[0] yellow
Native PowerShell
ToLookup
groups objects by a certain key… which is just what Group-Object
does when you specify the -AsHashTable
parameter.
# Use the same setup as above, then just...
PS> $groups = $colors | Group-Object -Property Length -AsHashTable
PS> $groups
Name Value
---- -----
6 {yellow, orange}
5 {green, black}
4 {blue}
3 {red}
Elements
First
Returns the first element of a sequence. Throws an exception if the sequence contains no elements. Note that evaluation stops at the first element in the sequence; the remainder of the sequence is not evaluated.
If you specify an optional condition, First
returns the first element in a sequence that satisfies that condition. Throws an exception if no elements satisfy the condition. Note that evaluation stops at the first element satisfying the condition in the sequence; the remainder of the sequence is not evaluated.
LINQ in C#
1 2 3 |
int[] numbers = { 2, 0, 5, -11, 29 }; var firstNumber = numbers.First(); var firstNumberConditionally = numbers.First(n => n > 4); |
LINQ in PowerShell
1 2 3 4 5 6 |
PS> [int[]] $numbers = @(2, 0, 5, -11, 29) PS> [Linq.Enumerable]::First($numbers) 2 PS> $delegate = [Func[int,bool]] { $args[0] -gt 4 } PS> [Linq.Enumerable]::First($numbers, $delegate) 5 |
Native PowerShell
1 2 3 4 5 |
PS> $numbers = @(2, 0, 5, -11, 29) PS> $numbers[0] 2 PS> ($numbers | Where-Object { $_ -gt 4 })[0] 5 |
FirstOrDefault
Returns the first element of a sequence, or a default value if the sequence contains no elements. Note that evaluation stops at the first element in the sequence; the remainder of the sequence is not evaluated.
If you specify an optional condition, FirstOrDefault
returns the first element in a sequence that satisfies that condition, or a default value if the sequence contains no elements. Note that evaluation stops at the first element satisfying the condition in the sequence; the remainder of the sequence is not evaluated.
LINQ in C#
1 2 3 |
int[] numbers = { 2, 0, 5, -11, 29 }; var firstNumber = numbers.FirstOrDefault(); var firstNumberConditionally = numbers.FirstOrDefault(n => n > 100); |
LINQ in PowerShell
1 2 3 4 5 6 |
PS> [int[]] $numbers = @(2, 0, 5, -11, 29) PS> [Linq.Enumerable]::FirstOrDefault($numbers) 2 PS> $delegate = [Func[int,bool]] { $args[0] -gt 100 } PS> [Linq.Enumerable]::First($numbers, $delegate) 0 |
Native PowerShell
1 2 3 4 5 |
PS> $numbers = @(2, 0, 5, -11, 29) PS> if ($numbers) { $ numbers[0] } else { 0 } 2 PS> $results = $numbers | Where { $_ -gt 100 }; if ($results) { $results[0] } else { 0 } 0 |
Last
Returns the last element of a sequence. Throws an exception if the sequence contains no elements. The entire sequence must be evaluated to get to the last element.
If you specify an optional condition, Last
returns the last element of a sequence that satisfies that condition. Throws an exception if the sequence contains no elements. The entire sequence must be evaluated to identify the target element, even if it ends up not being the actual last one in the sequence.
LINQ in C#
1 2 3 |
int[] numbers = { 2, 0, 5, -11, 29 }; var lastNumber = numbers.Last(); var lastNumberConditionally = numbers.Last(n => n < 5); |
LINQ in PowerShell
1 2 3 4 5 6 |
PS> [int[]] $numbers = @(2, 0, 5, -11, 29) PS> [Linq.Enumerable]::Last($numbers) 29 PS> $delegate = [Func[int,bool]] { $args[0] -lt 5 } PS> [Linq.Enumerable]::Last($numbers, $delegate) -11 |
Native PowerShell
1 2 3 4 5 |
PS> $numbers = @(2, 0, 5, -11, 29) PS> $numbers | Select-Object -last 1 29 PS> $numbers | Where-Object { $_ -lt 5 } | Select-Object -last 1 -11 |
LastOrDefault
Returns the last element of a sequence, or a default value if the sequence contains no elements. The entire sequence must be evaluated to get to the last element.
If you specify an optional condition, LastOrDefault
returns the last element of a sequence that satisfies that condition, or a default value if the sequence contains no elements. The entire sequence must be evaluated to identify the target element, even if it ends up not being the actual last one in the sequence.
LINQ in C#
1 2 3 |
int[] numbers = { 2, 0, 5, -11, 29 }; var lastNumber = numbers.LastOrDefault(); var lastNumberConditionally = numbers.LastOrDefault(n => n < 5); |
LINQ in PowerShell
1 2 3 4 5 6 |
PS> [int[]] $numbers = @(2, 0, 5, -11, 29) PS> [Linq.Enumerable]::LastOrDefault($numbers) 29 PS> $delegate = [Func[int,bool]] { $args[0] -gt 1000 } PS> [Linq.Enumerable]::LastOrDefault($numbers, $delegate) 0 |
Native PowerShell
1 2 3 4 5 |
PS> $numbers = @(2, 0, 5, -11, 29) PS> if ($numbers) { $numbers | Select-Object -last 1 } else { 0 } 29 PS> $result = $numbers | Where-Object { $_ -gt 1000 }; if ($result) { $result | Select-Object -last 1 } else { 0 } 0 |
ElementAt
Returns the element at a specified index (zero-based) in a sequence. Throws an exception if the index is out of range.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var thirdValue = stringData.ElementAt(2); |
LINQ in PowerShell
1 2 3 |
PS> [string[]] $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> [Linq.Enumerable]::ElementAt($StringData, 2) tri |
Native PowerShell
1 2 3 |
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> $StringData[2] tri |
ElementAtOrDefault
Returns the element at a specified index (zero-based) in a sequence, or a default value if the index is out of range.
LINQ in C#
1 2 |
int[] numbers = { 2, 0, 5, -11, 29 }; var absentValue = numbers.ElementAtOrDefault(99); |
LINQ in PowerShell
1 2 3 |
PS> [int[]] $numbers = @(2, 0, 5, -11, 29) PS> [Linq.Enumerable]::ElementAtOrDefault($numbers, 99) 0 |
Native PowerShell
1 2 3 |
PS> $numbers = @(2, 0, 5, -11, 29) PS> if ($numbers.Length -gt 99) { $numbers[99] } else { 0 } 0 |
Single
Returns the only element of a sequence. Throws an exception if the sequence contains more than one element.
If you specify an optional condition, Single
returns the only element in a sequence that satisfies that condition. Throws an exception if either no elements or more than one element satisfy the condition.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var fourCharWord = stringData.Single(w => w.Length == 4); |
LINQ in PowerShell
1 2 3 4 |
PS> [string[]]$StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> $delegate = [Func[string,bool]] { $args[0].Length -eq 4 } PS> [Linq.Enumerable]::Single($StringData, $delegate) pymp |
Native PowerShell
1 2 3 4 5 |
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> $result = @($StringData | Where { $_.Length -eq 4 }) # force into an array PS> if ($result.Length -ne 1) { throw “Sequence does NOT contain just one element" } PS> $result pymp |
SingleOrDefault
Returns the only element of a sequence or a default value if the sequence is empty. Throws an exception if the sequence contains more than one element.
If you specify an optional condition, SingleOrDefault
returns the only element in a sequence that satisfies that condition, or a default value if the sequence is empty. Throws an exception if the sequence contains more than one element.
LINQ in C#
1 2 |
int[] numbers = { 2, 0, 5, -11, 29 }; var absentValue = numbers.SingleOrDefault(n => n > 42); |
LINQ in PowerShell
1 2 3 4 |
PS> [int[]] $numbers = @(2, 0, 5, -11, 29) PS> $delegate = [Func[int,bool]] { $args[0] -gt 42 } PS> [Linq.Enumerable]::SingleOrDefault($numbers, $delegate) 0 |
Native PowerShell
PS> $numbers = @(2, 0, 5, -11, 29)
PS> $result = @($numbers | Where { $_ -gt 42 }) # force into an array
PS> if ($result.Length -gt 1) { throw “Sequence contains more than one element" }
PS> if ($result.Length -eq 1) { $result[0] } else { 0 }
0
Generation
Range
Generates a sequence of integral numbers within a specified range.
LINQ in C#
1 |
var numbers = Enumerable.Range(0,5); |
LINQ in PowerShell
1 2 3 4 5 6 |
PS> [Linq.Enumerable]::Range(0,5) 0 1 2 3 4 |
Native PowerShell
1 2 3 4 5 6 |
PS> 0..4 0 1 2 3 4 |
Repeat
Generates a sequence that contains a repeated value a specified number of times.
LINQ in C#
1 |
var numbers = Enumerable.Repeat(“one”, 3); |
LINQ in PowerShell
1 2 3 4 |
PS> [Linq.Enumerable]::Repeat("one", 3) one one one |
Native PowerShell
1 2 3 4 |
PS> 1..3 | ForEach-Object { "one" } one one one |
Empty
Returns an empty IEnumerable<T>
that has the specified type argument.
LINQ in C#
1 |
var emptyList = Enumerable.Empty<string>(); |
LINQ in PowerShell
1 2 3 4 |
var emptyForString = typeof(System.Linq.Enumerable) .GetMethod("Empty") .MakeGenericMethod(typeof(string)); var emptyList = emptyForString.Invoke(null, new object[] { }); |
And that translates to PowerShell as:
PS> $stringType = "".GetType() # set to your target type PS> $emptyForString = [Linq.Enumerable].GetMethod("Empty").MakeGenericMethod($stringType) # The last comma below wraps the array arg $stuff within another array PS> $emptyList = $emptyForString.Invoke($null, @()) PS> $emptyList.Count 0 PS> $emptyList.GetType().name String[]
Native PowerShell
1 |
PS> [string[]]$emptyList = @() |
DefaultIfEmpty
Returns the default value of the sequence’s elements (or, if a type parameter is explicitly specified, that type’s default value) in a singleton collection if the sequence is empty. In this first example, the list is not empty so it returns the original sequence.
LINQ in C#
1 2 3 4 |
string[] words = { "one","two","three" }; var result = words .Where(w => w.Length == 3) .DefaultIfEmpty("unknown"); |
LINQ in PowerShell
1 2 3 4 5 6 7 |
PS> [string[]] $words = @( "one","two","three" ) PS> $filteredWords = [Linq.Enumerable]::Where( $words, [Func[string,bool]] { $args[0].Length -eq 3 }) PS> [Linq.Enumerable]::DefaultIfEmpty($filteredWords, "unknown") one two |
But if the condition is changed so the filtered list has no elements, then:
1 2 3 4 5 |
PS> $filteredWords = [Linq.Enumerable]::Where( $words, [Func[string,bool]] { $args[0].Length -eq 2 }) PS> [Linq.Enumerable]::DefaultIfEmpty($filteredWords, "unknown") unknown |
Native PowerShell
PS> $words = @( "one","two","three" )
PS> $filteredWords = $words | Where-Object { $_.Length -eq 2 }
PS> if ($filteredWords) { $filteredWords } else { "unknown" }
one
two
# Again, the filter is changed here to now filter out all items
PS> $filteredWords = $words | Where-Object { $_.Length -eq 3 }
unknown
Grouping
GroupBy
Groups the elements of a sequence according to a specified key selector function (pet =>
pet.Age
). In the result, the second group is expanded to show its contents, containing 2 members of age 4. Notice that the elements of the group are objects of the original type, Pet
.
LINQ in C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Pet { public string Name { get; set; } public int Age { get; set; } } var pets = new List<Pet>{ new Pet {Name="Barley", Age=8}, new Pet {Name="Boots", Age=4}, new Pet {Name="Whiskers",Age=1}, new Pet {Name="Daisy", Age=4} }; var resultA = pets.GroupBy(pet => pet.Age); var resultB = pets.GroupBy(pet => pet.Age, pet => pet.Name); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Pet { [string] $Name; [int] $Age; Pet([string]$name, [int] $age) { $this.Name = $name $this.Age = $age } } [Pet[]]$pets = @( [Pet]::new("Barley", 8), [Pet]::new("Boots", 4), [Pet]::new("Whiskers", 1), [Pet]::new("Daisy", 4) ) PS> $ageDelegate = [Func[Pet,int]] { $args[0].Age } PS> $groupQueryA = [Linq.Enumerable]::GroupBy($pets, $ageDelegate) PS> $groupQueryA Name Age ---- --- Barley 8 Boots 4 Daisy 4 Whiskers 1 PS> $groups = [Linq.Enumerable]::ToArray($groupQueryA) PS> $groups[1] Name Age ---- --- Boots 4 Daisy 4 |
If you specify an optional projection function, GroupBy
further projects the elements for each group with that function (pet =>
pet.Name
in this next example). In the result, the second group is expanded to show its contents, containing 2 members of age 4. Notice that the elements of the group are now comprised of just the projected property, the pet’s name.
1 2 3 4 5 6 7 8 9 10 11 |
PS> $nameDelegate = [Func[Pet,string]] { $args[0].Name } PS> $groupQueryB = [Linq.Enumerable]::GroupBy($pets, $ageDelegate, $nameDelegate) PS> $groupQueryB Barley Boots Daisy Whiskers PS> $groups = [Linq.Enumerable]::ToArray($groupQueryB) PS> $groups[1] Boots Daisy |
Native PowerShell
# Use the same setup as above, then just...
PS> $groups = $pets | Group-Object -Property Age
Count Name Group
----- ---- -----
1 8 {Pet}
2 4 {Pet, Pet}
1 1 {Pet}
PS> $groups[1].Group
Name Age
---- ---
Boots 4
Daisy 4
Join
Cross Join
Correlates the elements of two sequences based on matching keys. If the first sequence has no corresponding elements in the second sequence, it is not represented in the result. Join
is equivalent to an inner join in SQL.
LINQ in C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class User { public int Id { get; set; } public string Name { get; set; } } class Book { public int Id { get; set; } public string Title { get; set; } } var users = new List<User> { new User{Id=1, Name = "Sam"}, new User{Id=6, Name = "Dean"}, new User{Id=3, Name = "Crowley"}, new User{Id=4, Name = "Chuck"}, new User{Id=5, Name = "Castiel"} }; var books = new List<Book> { new Book{Id = 3, Title = "Inferno"}, new Book{Id = 9, Title = "Bliss"}, new Book{Id = 5, Title = "Heaven Can Wait"}, new Book{Id = 1, Title = "Beowulf"}, new Book{Id = 6, Title = "Bates Motel"} }; var booksMatchedWithUsers = users.Join( books, u => u.Id, b => b.Id, (u, b) => $"{u.Name} => {b.Title}" ); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
class User { [int] $Id; [string] $Name; User($id, $name) { $this.Id = $id $this.Name = $name } } class Book { [int] $Id; [string] $Title; Book($id, $title) { $this.Id = $id $this.Title = $title } } [User[]]$users = @( [User]::new(1, "Sam"), [User]::new(6, "Dean"), [User]::new(3, "Crowley"), [User]::new(4, "Chuck"), [User]::new(5, "Castiel") ) [Book[]]$books = @( [Book]::new(3, "Inferno"), [Book]::new(9, "Bliss"), [Book]::new(5, "Heaven Can Wait"), [Book]::new(1, "Beowulf"), [Book]::new(6, "Bates Motel") ) PS> $outerKeyDelegate = [Func[User,int]] { $args[0].Id } PS> $innerKeyDelegate = [Func[Book,int]] { $args[0].Id } PS> $resultDelegate = [Func[User,Book,string]] { '{0} => {1}' -f $args[0].Name, $args[1].Title } PS> [Linq.Enumerable]::Join( $users, $books, $outerKeyDelegate, $innerKeyDelegate, $resultDelegate) Sam => Beowulf Dean => Bates Motel Crowley => Inferno Castiel => Heaven Can Wait |
Native PowerShell
# Use the same setup as above, then just...
PS> $users | ForEach-Object {
$user = $_
$book = $books | Where-Object Id -eq $user.Id
if ($book) { "{0} => {1}" -f $user.Name, $book.Title }
}
Sam => Beowulf
Dean => Bates Motel
Crowley => Inferno
Castiel => Heaven Can Wait
Group Join
Correlates the elements of two sequences based on equality of keys and groups the results. If the first sequence has no corresponding elements in the second sequence, it is still represented in the result but its group contains no members. In the example, notice that user Chuck (id=4) has no books associated with him. Group Join
is equivalent to a left outer join in SQL.
LINQ in C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class User { public int Id { get; set; } public string Name { get; set; } } class Book { public int Id { get; set; } public string Title { get; set; } } var users = new List<User> { new User{Id=1, Name = "Sam"}, new User{Id=6, Name = "Dean"}, new User{Id=3, Name = "Crowley"}, new User{Id=4, Name = "Chuck"}, new User{Id=5, Name = "Castiel"} }; var books = new List<Book> { new Book{Id = 3, Title = "Inferno"}, new Book{Id = 1, Title = "Inferno"}, new Book{Id = 9, Title = "Bliss"}, new Book{Id = 5, Title = "Heaven Can Wait"}, new Book{Id = 1, Title = "Beowulf"}, new Book{Id = 6, Title = "Bates Motel"} }; var booksMatchedWithUsers = users.GroupJoin( books, u => u.Id, b => b.Id, (u, bList) => $"{u.Name} => {bList.Count()}" ); |
LINQ in PowerShell
While this example is extremely similar to that for Cross Join
above, it has one key change—requiring a list in the delegate—that is inexplicably causing it to fail, as noted towards the bottom.
class User
{
[int] $Id;
[string] $Name;
User($id, $name) {
$this.Id = $id
$this.Name = $name
}
}
class Book
{
[int] $Id;
[string] $Title;
Book($id, $title) {
$this.Id = $id
$this.Title = $title
}
}
[User[]]$users = @(
[User]::new(1, "Sam"),
[User]::new(6, "Dean"),
[User]::new(3, "Crowley"),
[User]::new(4, "Chuck"),
[User]::new(5, "Castiel")
)
[Book[]]$books = @(
[Book]::new(3, "Inferno"),
[Book]::new(1, "Inferno"),
[Book]::new(9, "Bliss"),
[Book]::new(5, "Heaven Can Wait"),
[Book]::new(1, "Beowulf"),
[Book]::new(6, "Bates Motel")
)
PS> $outerKeyDelegate = [Func[User,int]] { $args[0].Id }
PS> $innerKeyDelegate = [Func[Book,int]] { $args[0].Id }
# Thanks to reader "ili" for deciphering the needed middle type here!
# Turns out we need an IEnumerable[Book] rather than Book[] as I tried.
PS> $resultDelegate = [Func[User,[Collections.Generic.IEnumerable[Book]],string]]
{ '{0} => {1}' -f $args[0].Name, $args[1].Count }
PS> [Linq.Enumerable]::GroupJoin(
$users, $books, $outerKeyDelegate, $innerKeyDelegate, $resultDelegate)
Sam => 2
Dean => 1
Crowley => 1
Chuck => 0
Castiel => 1
Concat
Concatenates two sequences into a single sequence; further LINQ operations would then operate on the new, combined sequence.
LINQ in C#
1 2 3 |
string[] words1 = { "one","two","three" }; string[] words2 = { "green","red","blue" }; var combined = words1.Concat(words2); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 |
var result = words .Where(w => w.Length == 3) .DefaultIfEmpty("unknown");one two three green red blue |
Native PowerShell
1 2 3 4 5 6 7 8 9 |
PS> $words1 = @( "one","two","three" ) PS> $words2 = @( "green","red","blue" ) PS> $words1 + $words2 one two three green red blue |
Zip
Applies a specified function to the corresponding elements of two sequences, producing a new sequence of the results. If the first sequence is longer than the second, one element past the common length will be evaluated (“d” in the example) at which point a determination is made that the second sequence has been consumed, and further evaluation stops (so “e” is not evaluated). If the second sequence is longer than the first, its extra values will not be evaluated at all. Note that “zip” in this context has nothing to do with zip archives!
LINQ in C#
1 2 3 |
string[] words = { "a", "b", "c", "d", "e" }; int[] numbers = { 1, 2, 3 }; var result = words.Zip(numbers, (w, n) => w + n); |
LINQ in PowerShell
1 2 3 4 5 6 7 |
PS> [string[]] $words = @( "a", "b", "c", "d", "e" ) PS> [int[]] $numbers = @( 1, 2, 3 ) PS> $delegate = [Func[string,int,string]] { $args[0] + $args[1] } PS> [Linq.Enumerable]::Zip($words, $numbers, $delegate) a1 b2 c3 |
Native PowerShell
1 2 3 4 |
PS> $words | foreach-object {$i=0} {if ($i -lt $numbers.Count) {$_ + $numbers[$i++]}} a1 b2 c3 |
Ordering
OrderBy
Sorts the elements of a sequence in ascending order according to a key selector function.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var sortedValues = stringData.OrderBy(word => word); |
LINQ in PowerShell
1 2 3 4 5 6 7 |
PS> [string[]] $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> [Linq.Enumerable]::OrderBy($StringData, [Func[string,string]] { $args[0] }) dew peswar pymp tri unn |
Native PowerShell
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp")
# simple strings; no properties need to be used; compare to next example
PS> $StringData | Sort-Object
dew
peswar
pymp
tri
unn
OrderByDescending
Sorts the elements of a sequence in descending order according to a key selector function.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var sortedValues = stringData.OrderByDescending(word => word.Length); |
LINQ in PowerShell
1 2 3 4 5 6 7 |
PS> [string[]] $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> [Linq.Enumerable]::OrderByDescending($StringData, [Func[string,string]] { $args[0].Length }) peswar pymp unn dew tri |
Native PowerShell
1 2 3 4 5 6 7 |
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> $stringData | Sort-Object -Property Length -Descending peswar pymp unn dew tri |
ThenBy
Performs a subsequent ordering of the elements in a sequence in ascending order according to a key selector function (d =>
d.Month
in this example). Note that unlike most other LINQ operators, which accept an IEnumerable<T>
input, ThenBy
accepts an IOrderedEnumerable<T>
input—which happens to be the output of OrderBy
.
LINQ in C#
1 2 3 4 5 6 7 |
var dates = new DateTime[] { new DateTime(2017, 10, 23), new DateTime(2016, 12, 3), new DateTime(2016, 2, 13) }; var result = dates.OrderBy(d => d.Year).ThenBy(d => d.Month); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 9 10 11 12 |
PS> [DateTime[]]$dates = (Get-Date -Year 2017 -Month 10 -Day 23), (Get-Date -Year 2016 -Month 12 -Day 3), (Get-Date -Year 2016 -Month 2 -Day 13) PS> $yearDelegate = [Func[DateTime,int]] { $args[0].Year } PS> $monthDelegate = [Func[DateTime,int]] { $args[0].Month } PS> [Linq.Enumerable]::ThenBy( [Linq.Enumerable]::OrderBy($dates, $yearDelegate), $monthDelegate) Saturday, February 13, 2016 4:22:31 PM Saturday, December 3, 2016 4:22:31 PM Monday, October 23, 2017 4:22:31 PM |
Native PowerShell
1 2 3 4 5 6 7 8 9 |
PS> [DateTime[]]$dates = (Get-Date -Year 2017 -Month 10 -Day 23), (Get-Date -Year 2016 -Month 12 -Day 3), (Get-Date -Year 2016 -Month 2 -Day 13) PS> $dates | Sort-Object -Property @{ Expression="Year"; Descending=$false }, @{ Expression="Month"; Descending=$false } Saturday, February 13, 2016 4:22:31 PM Saturday, December 3, 2016 4:22:31 PM Monday, October 23, 2017 4:22:31 PM |
ThenByDescending
Performs a subsequent ordering of the elements in a sequence in descending order according to a key selector function. Note that unlike most other LINQ operators, which accept an IEnumerable<T>
input, ThenBy
accepts an IOrderedEnumerable<T>
input—which happens to be the output of OrderBy
.
Works identically to ThenBy
except you set the Descending property to true in the native PowerShell example.
Reverse
Inverts the order of the elements in a sequence.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var reversedValues = stringData.Reverse(); |
LINQ in PowerShell
1 2 3 4 5 6 7 |
PS> [string[]] $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> [Linq.Enumerable]::Reverse($StringData) pymp peswar tri dew unn |
Native PowerShell
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp") # Careful! This call modifies the *original* array and does *not* output it. PS> [array]::Reverse($StringData) # Then to see the output: PS> $StringData pymp peswar tri dew unn
Partitioning
Take
Returns a specified number of elements from the start of a sequence. Evaluation of the sequence stops after that as no further elements are needed.
LINQ in C#
1 2 |
int[] numbers = Enumerable.Range(1, 10); var firstHalfQuery = numbers.Take(5); |
LINQ in PowerShell
1 2 |
[int[]]$numbers = 1..10 $firstHalfQuery = [Linq.Enumerable]::Take($numbers, 5) |
Native PowerShell
1 2 3 |
[int[]]$numbers = 1..10 $firstHalf = $numbers[0..4] $firstHalf = $numbers | Select-Object -First 5 |
Skip
Bypasses a specified number of elements in a sequence and then returns the remaining elements.
LINQ in C#
1 2 |
int[] numbers = Enumerable.Range(1, 10); var lastHalfQuery = numbers.Skip(5); |
LINQ in PowerShell
1 2 |
[int[]]$numbers = 1..10 $lastHalfQuery = [Linq.Enumerable]::Skip($numbers, 5) |
Native PowerShell
1 2 3 |
[int[]]$numbers = 1..10 $lastHalf = $numbers[5..9] $lastHalf = $numbers | Select-Object -Skip 5 |
TakeWhile
Returns elements from the start of a sequence as long as a specified condition is true. Evaluation of the sequence stops after that as no further elements are needed.
LINQ in C#
1 2 |
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }; var initialNumbersUntil7trigger = numbers.TakeWhile(n => n < 7); |
LINQ in PowerShell
1 2 3 |
[int[]]$numbers = @(5, 4, 1, 3, 9, 8, 6, 7, 2, 0); $delegate = [Func[int,bool]] { $args[0] -lt 7 } $initialNumbersUntil7trigger = [Linq.Enumerable]::TakeWhile($numbers, $delegate) |
Native PowerShell
PowerShell does not have an equivalent one-liner to do a TakeWhile
, but with the Take-While function created by JaredPar, you could just do this:
1 2 |
[int[]]$numbers = @(5, 4, 1, 3, 9, 8, 6, 7, 2, 0); $initialNumbersUntil7trigger = $numbers | Take-While {$args[0] -lt 7} |
SkipWhile
Bypasses elements in a sequence as long as a specified condition is true and then returns the remaining elements.
LINQ in C#
1 2 |
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }; var numbersAfter7trigger = numbers.SkipWhile(n => n < 7); |
LINQ in PowerShell
1 2 3 |
[int[]]$numbers = @(5, 4, 1, 3, 9, 8, 6, 7, 2, 0); $delegate = [Func[int,bool]] { $args[0] -lt 7 } $numbersAfter7trigger = [Linq.Enumerable]::SkipWhile($numbers, $delegate) |
Native PowerShell
PowerShell does not have an equivalent one-liner to do a SkipWhile
, but with the Skip-While function created by JaredPar, you could just do this:
1 2 |
[int[]]$numbers = @(5, 4, 1, 3, 9, 8, 6, 7, 2, 0); $numbersAfter7trigger = $numbers | Skip-While {$args[0] -lt 7} |
Projection
Select
Applies a specified transformation to each element of a sequence; this transformation is generally referred to as “projection”. Often you might project into a new object that is a subset of the original object, essentially discarding unneeded properties. In the illustration, the sequence is transformed to a new sequence with just the DayOfYear
property.
LINQ in C#
1 2 3 4 5 6 7 |
var dates = new DateTime[] { new DateTime(2017, 10, 23), new DateTime(2013, 12, 3), new DateTime(2016, 2, 13) }; var result = dates.Select(d => d.DayOfYear); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 |
PS> [DateTime[]]$dates = (Get-Date -Year 2017 -Month 10 -Day 23), (Get-Date -Year 2013 -Month 12 -Day 3), (Get-Date -Year 2016 -Month 2 -Day 13) PS> [Linq.Enumerable]::Select($dates, [Func[DateTime,int]] { $args[0].DayOfYear}) 296 337 44 |
Native PowerShell
1 2 3 4 5 6 7 8 |
PS> [DateTime[]]$dates = (Get-Date -Year 2017 -Month 10 -Day 23), (Get-Date -Year 2013 -Month 12 -Day 3), (Get-Date -Year 2016 -Month 2 -Day 13) PS> $dates | Select-Object -ExpandProperty DayOfYear 296 337 44 |
SelectMany
Projects each element of a sequence to an IEnumerable<T>
and flattens the resulting sequences into a single sequence. If, in the illustration, Select
had been used instead of SelectMany
, each element of the result would be a list of User
objects (i.e. a list of string arrays) rather than a list of strings, as shown, and the result would be just a 2-element list rather than a 6 element list.
LINQ in C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class DayTally { public DateTime Day { get; set; } public string[] User { get; set; } } IEnumerable<string> Process() { var days = new List<DayTally> { new DayTally{ Day = new DateTime(2017, 10, 23), User = new[] { "user1", "user2", "user5", "user4" } }, new DayTally{ Day = new DateTime(2017, 2, 5), User = new[] { "user3", "user6" } } }; return days.SelectMany(d => d.User); } |
LINQ in PowerShell
class DayTally
{
[DateTime] $Day;
[string[]] $User;
DayTally([DateTime] $day, [string[]] $user) {
$this.Day = $day;
$this.User = $user;
}
}
[DayTally[]]$days = @(
[DayTally]::new(
(Get-Date -Year 2017 -Month 10 -Day 23),
[string[]] @( "user1", "user2", "user5", "user4" ));
[DayTally]::new(
(Get-Date -Year 2017 -Month 2 -Day 5),
[string[]] @( "user3", "user6" ));
)
# Careful with the delegate signature! E.g. change 'string[]' to 'string' and watch what happens
PS> [Func[DayTally,string[]]] $delegate = { return $args[0].User }
PS> [Linq.Enumerable]::SelectMany($days, $delegate)
user1
user2
user5
user4
user3
user6
Native PowerShell
# Use the same setup as above, then just...
PS> $days | Select -ExpandProperty User
user1
user2
user5
user4
user3
user6
Quantifiers
Any
Determines whether any element of a sequence (i.e. at least one element) satisfies a condition. All elements of the sequence need to be evaluated to provide a false
result (first figure). However, if at any time during evaluating the sequence an element evaluates to true
, the sequence evaluation stops at that element (second figure). Of course, if only the last element satisfies the condition, all elements will need to be evaluated and true
will be returned.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var anyPresent = stringData.Any(s => Regex.IsMatch(s, ".*war")); |
LINQ in PowerShell
1 2 3 |
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> $delegate = [Func[string,bool]]{ $args[0] -match '.*war' } PS> [Linq.Enumerable]::Any([string[]]$StringData, $delegate) |
Native PowerShell
PowerShell does not have an equivalent one-liner to do Any
, but there are a variety of suggestions to implement Test-Any
in this StackOverflow post. The key is stopping the pipeline once you make a determination; see the discussion on StackOverflow for details.
All
Determines whether all elements of a sequence satisfy a condition. All elements of the sequence need to be evaluated to provide a true
result (first figure). However, if at any time during evaluating the sequence an element evaluates to false
, the sequence evaluation stops at that element (second figure). Of course, if only the last element fails to satisfy the condition, all elements will need to be evaluated and false
will be returned.
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var allPresent = stringData.All(s => s.Contains("e")); |
LINQ in PowerShell
1 2 3 |
PS> $StringData = @("unn", "dew", "tri", "peswar", "pymp") PS> $delegate = [Func[string,bool]]{ $args[0].Contains("e") } PS> [Linq.Enumerable]::Any([string[]]$StringData, $delegate) |
Native PowerShell
PowerShell does not have an equivalent one-liner to do All
, but this StackOverflow post shows how to implement a Test-All
function. (Note, however, that that function does not optimizing performance in terms of stopping the pipeline once a determination is made; see comments on Any
.)
Contains
Determines whether a sequence contains a specified element. The sequence may, of course, contain objects of an arbitrary type. In the case of strings, however, note that this method matches against each element in its entirety. Contrast this to the string method Contains
that determines whether a string matches against a substring. (See the example for All
.)
LINQ in C#
1 2 |
var stringData = new[] { "unn", "dew", "tri", "peswar", "pymp" }; var result = stringData.Contains("dew"); |
LINQ in PowerShell
1 2 |
[string[]] $StringData = @("unn", "dew", "tri", "peswar", "pymp") [Linq.Enumerable]::Contains($StringData, "dew") |
Native PowerShell
1 2 |
$StringData = @("unn", "dew", "tri", "peswar", "pymp") $StringData -contains "dew" |
SequenceEqual
Determines whether two sequences are equal; specifically, if the two sequences contain the same elements in the same order. When dealing with value types, as in the illustration, the use is intuitive: the lists differ at the third position so a determination has been made that they are different, and no further elements of the sequence need to be evaluated. Note that if you use reference types, the elements are matched with reference equality; they need to be the actual, same object, not just objects with all the same property values
LINQ in C#
1 2 3 |
int[] num1 = { 3, 1, 4, 1, 5 }; int[] num2 = { 3, 1, 5, 1, 4 }; var result = num1.SequenceEqual(num2); |
LINQ in PowerShell
1 2 3 |
[int[]] $num1 = @(3, 1, 4, 1, 5 ); [int[]] $num2 = @(3, 1, 5, 1, 4 ); [Linq.Enumerable]:: SequenceEqual($num1, $num2) |
Native PowerShell
This is similar, in that it compares two sequences, but it is order-independent. This example will return true here while the LINQ expression above returned false.
1 2 3 |
$num1 = @(3, 1, 4, 1, 5 ); $num2 = @(3, 1, 5, 1, 4 ); [bool]((Compare-Object -Reference $num1 -Difference $num2) -eq $null) |
Restriction (Filtering)
Where
Filters a sequence of values based on a predicate.
LINQ in C#
1 2 3 4 5 6 7 |
var dates = new DateTime[] { new DateTime(2017, 10, 23), new DateTime(2013, 12, 3), new DateTime(2016, 2, 13) }; var result = dates.Where(d => d.Year > 2016); |
LINQ in PowerShell
1 2 3 4 5 6 7 |
PS> [DateTime[]]$dates = (Get-Date -Year 2017 -Month 10 -Day 23), (Get-Date -Year 2013 -Month 12 -Day 3), (Get-Date -Year 2016 -Month 2 -Day 13) PS> [Func[DateTime,bool]] $delegate = { param($d); return $d.Year -gt 2016 } PS> [Linq.Enumerable]::Where($dates, $delegate) Monday, October 23, 2017 3:38:48 PM |
Native PowerShell
1 2 3 4 5 6 |
PS> [DateTime[]]$dates = (Get-Date -Year 2017 -Month 10 -Day 23), (Get-Date -Year 2013 -Month 12 -Day 3), (Get-Date -Year 2016 -Month 2 -Day 13) PS> $dates | Where-Object Year -gt 2016 Monday, October 23, 2017 3:38:48 PM |
Sets
Distinct
Returns distinct elements from a sequence. Note that the sequence does not need to be sorted.
LINQ in C#
1 2 |
int[] factorsOf300 = { 2, 3, 5, 2, 5}; int uniqueFactors = factorsOf300.Distinct(); |
LINQ in PowerShell
1 2 3 4 5 |
PS> [int[]] $factorsOf300 = @(2, 3, 5, 2, 5) PS> [Linq.Enumerable]::Distinct($factorsOf300) 2 3 5 |
Native PowerShell
1 2 3 4 5 |
PS> $factorsOf300 = @(2, 3, 5, 2, 5) PS> $factorsOf300 | Select-Object -Unique 2 3 5 |
Union
Produces the set union of two sequences. Includes elements in both sequences but without duplication.
LINQ in C#
1 2 3 |
int[] numbersA = { 0, 2, 4, 5 }; int[] numbersB = { 5, 2, 7, 1 }; var uniqueNumbers =numbersA.Union(numbersB); |
LINQ in PowerShell
1 2 3 4 5 6 7 8 9 |
PS> [int[]] $numbersA = @(0, 2, 4, 5) PS> [int[]] $numbersB = @(5, 2, 7, 1) PS> [Linq.Enumerable]::Union($numbersA, $numbersB) 0 2 4 5 7 1 |
Native PowerShell
1 2 3 4 5 6 7 8 9 |
PS> $numbersA = @(0, 2, 4, 5) PS> $numbersB = @(5, 2, 7, 1) PS> $numbersA + $numbersB | Select-Object -Unique 0 2 4 5 7 1 |
Intersection
Produces the set intersection of two sequences. Just those elements that exist in both sequences appear in the result.
LINQ in C#
1 2 3 |
int[] numbersA = { 0, 2, 4, 5 }; int[] numbersB = { 5, 2, 7, 1 }; var commonNumbers =numbersA.Intersect(numbersB); |
LINQ in PowerShell
1 2 3 4 5 |
PS> [int[]] $numbersA = @(0, 2, 4, 5) PS> [int[]] $numbersB = @(5, 2, 7, 1) PS> [Linq.Enumerable]::Intersect($numbersA, $numbersB) 2 5 |
Native PowerShell
1 2 3 4 5 |
PS> $numbersA = @(0, 2, 4, 5) PS> $numbersB = @(5, 2, 7, 1) PS> $numbersA | Select-Object -Unique | Where-Object { $numbersB -contains $_ } 2 5 |
Except
Produces the set difference of one sequence with a second sequence. Just those elements that exist in the first sequence and do not exist in the second sequence appear in the result.
LINQ in C#
1 2 3 |
int[] numbersA = { 0, 2, 4, 5, 8 }; int[] numbersB = { 5, 2, 7, 1 }; var aOnlyNumbers = numbersA.Except(numbersB); |
LINQ in PowerShell
1 2 3 4 5 6 |
PS> [int[]] $numbersA = @(0, 2, 4, 5, 8) PS> [int[]] $numbersB = @(5, 2, 7, 1) PS> [Linq.Enumerable]::Except($numbersA, $numbersB) 0 4 8 |
Native PowerShell
1 2 3 4 5 6 |
PS> $numbersA = @(0, 2, 4, 5, 8) PS> $numbersB = @(5, 2, 7, 1) PS> $numbersA | Select-Object -Unique | Where-Object { $numbersB -notcontains $_ } 0 4 8 |
Conclusion
So, yes! LINQ can be done in PowerShell. Depending on the operator, it can be rather burdensome to do so. PowerShell is designed to be quick and easy to use, so be sure that you need the performance boost that LINQ can offer and, indeed, make sure that there is a performance boost for your data, and most importantly that the resulting data is correct. As part of your analysis, you may find it useful to have the accompanying wallchart that, for example, shows you at a glance which operators use deferred execution and which use immediate execution. Click here to download the PDF reference chart.
Load comments