There are many standard query operators in LINQ, only some of which are supported directly in C# query expressions—the others have to be called “manually” as normal methods. Some of the standard query operators are demonstrated in the main text of the book, but they’re all listed in this appendix. For the examples, I’ve defined two sample sequences:
string[] words = {"zero", "one", "two", "three", "four"}; int[] numbers = {0, 1, 2, 3, 4};
For completeness I’ve included the operators we’ve already seen, although in most cases chapter 11 contains more detail on them than I’ve provided here. For each operator, I’ve specified whether it uses deferred or immediate execution.
The aggregation operators (see table A.1) all result in a single value rather than a sequence. Average
and Sum
all operate either on a sequence of numbers (any of the built-in numeric types) or on a sequence of elements with a delegate to convert from each element to one of the built-in numeric types. Min
and Max
have overloads for numeric types, but can also operate on any sequence either using the default comparer for the element type or using a conversion delegate. Count
and LongCount
are equivalent to each other, just with different return types. Both of these have two overloads—one that just counts the length of the sequence, and one that takes a predicate: only elements matching the predicate are counted.
Table A.1. Examples of aggregation operators
Expression | Result |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
numbers.Aggregate("seed", (soFar, elt) => soFar+elt.ToString(), result => result.ToUpper()) |
|
The most generalized aggregation operator is just called Aggregate
. All the other aggregation operators could be expressed as calls to Aggregate
, although it would be relatively painful to do so. The basic idea is that there’s always a “result so far,” starting with an initial seed. An aggregation delegate is applied for each element of the input sequence: the delegate takes the result so far and the input element, and produces the next result. As a final optional step, a conversion is applied from the aggregation result to the return value of the method. This conversion may result in a different type, if necessary. It’s not quite as complicated as it sounds, but you’re still unlikely to use it very often.
All of the aggregation operators use immediate execution.
There is a single concatenation operator: Concat
(see table A.2). As you might expect, this operates on two sequences, and returns a single sequence consisting of all the elements of the first sequence followed by all the elements of the second. The two input sequences must be of the same type, and execution is deferred.
The conversion operators (see table A.3) cover a fair range of uses, but they all come in pairs. AsEnumerable
and AsQueryable
allow a sequence to be treated as IEnumerable <T>
or IQueryable
respectively, forcing further calls to convert lambda expressions into delegate instances or expression trees respectively, and use the appropriate extension methods. These operators use deferred execution.
Table A.3. Conversion examples
Expression | Result |
---|---|
allStrings.Cast<string>() | "These", "are", "all", "strings" (as IEnumerable<string>) |
allStrings.OfType<string>() | "These", "are", "all", "strings" (as IEnumerable<string>) |
notAllStrings.Cast<string>() | Exception is thrown while iterating, at point of failing conversion |
notAllStrings.OfType<string>() | "Number", "at", "the", "end" (as IEnumerable<string>) |
numbers.ToArray() | 0, 1, 2, 3, 4 (as int[]) |
numbers.ToList() | 0, 1, 2, 3, 4 (as List<int>) |
words.ToDictionary(word => word.Substring(0, 2) ) | Dictionary contents: "ze": "zero" "on": "one" "tw": "two" "th": "three" "fo": "four" |
// Key is first character of word words.ToLookup(word => word[0]) | Lookup contents: 'z': "zero" 'o': "one" 't': "two", "three" 'f': "four" |
words.ToDictionary(word => word[0]) | Exception: Can only have one entry per key, so fails on 't' |
ToArray
and ToList
are fairly self-explanatory: they read the whole sequence into memory, returning it either as an array or as a List<T>
. Both use immediate execution.
Cast
and OfType
convert an untyped sequence into a typed one, either throwing an exception (for Cast
) or ignoring (for OfType
) elements of the input sequence that aren’t implicitly convertible to the output sequence element type. This may also be used to convert typed sequences into more specifically typed sequences, such as converting IEnumerable<object>
to IEnumerable<string>
. The conversions are performed in a streaming manner with deferred execution.
ToDictionary
and ToLookup
both take delegates to obtain the key for any particular element; ToDictionary
returns a dictionary mapping the key to the element type, whereas ToLookup
returns an appropriately typed ILookup<,>
. A lookup is like a dictionary where the value associated with a key isn’t one element but a sequence of elements. Lookups are generally used when duplicate keys are expected as part of normal operation, whereas a duplicate key will cause ToDictionary
to throw an exception. More complicated overloads of both methods allow a custom IEqualityComparer<T>
to be used to compare keys, and a conversion delegate to be applied to each element before it is put into the dictionary or lookup.
The examples in table A.3 use two additional sequences to demonstrate Cast
and OfType
:
object[] allStrings = {"These", "are", "all", "strings"}; object[] notAllStrings = {"Number", "at", "the", "end", 5};
I haven’t provided examples for AsEnumerable
or AsQueryable
because they don’t affect the results in an immediately obvious way. Instead, they affect the manner in which the query is executed. Queryable.AsQueryable
is an extension method on IEnumerable
that returns an IQueryable
(both types being generic or nongeneric, depending on which overload you pick). If the IEnumerable
you call it on is already an IQueryable
, it just returns the same reference—otherwise it creates a wrapper around the original sequence. The wrapper allows you to use all the normal Queryable
extension methods, passing in expression trees, but when the query is executed the expression tree is compiled into normal IL and executed directly, using the LambdaExpression.Compile
method shown in section 9.3.2.
Enumerable.AsEnumerable
is an extension method on IEnumerable<T>
and has a trivial implementation, simply returning the reference it was called on. No wrappers are involved—it just returns the same reference. This forces the Enumerable
extension methods to be used in subsequent LINQ operators. Consider the following query expressions:
// Filter the users in the database with LIKE
from user in context.Users
where user.Name.StartsWith("Tim")
select user;
// Filter the users in memory
from user in context.Users.AsEnumerable()
where user.Name.StartsWith("Tim")
select user;
The second query expression forces the compile-time type of the source to be IEnumerable<User>
instead of IQueryable<User>
, so all the processing is done in memory instead of at the database. The compiler will use the Enumerable
extension methods (taking delegate parameters) instead of the Queryable
extension methods (taking expression tree parameters). Normally you want to do as much processing as possible in SQL, but when there are transformations that require “local” code, you sometimes have to force LINQ to use the appropriate Enumerable
extension methods.
This is another selection of query operators that are grouped in pairs (see table A.4). This time, the pairs all work the same way. There’s a simple version that picks a single element if it can or throws an exception if the specified element doesn’t exist, and a version with OrDefault
at the end of the name. The OrDefault
version is exactly the same except that it returns the default value for the result type instead of throwing an exception if it can’t find the element you’ve asked for. All of these operators use immediate execution.
Table A.4. Single element selection examples
Expression | Result |
---|---|
|
|
|
|
|
|
|
|
| Exception: No matching elements |
words.FirstOrDefault (word => word.Length==10) | null |
|
|
| Exception: More than one element |
|
|
|
|
| Exception: No matching elements |
The operator names are easily understood: First
and Last
return the first and last elements of the sequence respectively (only defaulting if there are no elements), Single
returns the only element in a sequence (defaulting if there isn’t exactly one element), and ElementAt
returns a specific element by index (the fifth element, for example). In addition, there’s an overload for all of the operators other than ElementAt
to filter the sequence first—for example, First
can return the first element that matches a given condition.
There’s only one equality operation: SequenceEqual
(see table A.5). This just compares two sequences for element-by-element equality, including order. For instance, the sequence 0, 1, 2, 3, 4 is not equal to 4, 3, 2, 1, 0. An overload allows a specific IEqualityComparer<T>
to be used when comparing elements. The return value is just a Boolean, and is computed with immediate execution.
Out of all the generation operators (see table A.6), only one acts on an existing sequence: DefaultIfEmpty
. This returns either the original sequence if it’s not empty, or a sequence with a single element otherwise. The element is normally the default value for the sequence type, but an overload allows you to specify which value to use.
Table A.6. Generation examples
Expression | Result |
---|---|
numbers.DefaultIfEmpty() | 0, 1, 2, 3, 4 |
new int[0].DefaultIfEmpty() | 0 (within an IEnumerable<int>) |
new int[0].DefaultIfEmpty(10) | 10 (within an IEnumerable<int>) |
Enumerable.Range(15, 2) | 15, 16 |
Enumerable.Repeat(25, 2) | 25, 25 |
Enumerable.Empty<int>() | An empty IEnumerable<int> |
There are three other generation operators that are just static methods in Enumerable
:
Range
generates a sequence of integers, with the parameters specifying the first value and how many values to generate.
Repeat
generates a sequence of any type by repeating a specified single value for a specified number of times.
Empty
generates an empty sequence of any type.
All of the generation operators use deferred execution.
There are two grouping operators, but one of them is ToLookup
(which we’ve already seen in A.3 as a conversion operator). That just leaves GroupBy
, which we saw in section 11.6.1 when discussing the group
... by
clause in query expressions. It uses deferred execution, but buffers results.
The result of GroupBy
is a sequence of appropriately typed IGrouping
elements. Each element has a key and a sequence of elements that match that key. In many ways, this is just a different way of looking at a lookup—instead of having random access to the groups by key, the groups are enumerated in turn. The order in which the groups are returned is the order in which their respective keys are discovered. Within a group, the order is the same as in the original sequence.
GroupBy
(see table A.7) has a daunting number of overloads, allowing you to specify not only how a key is derived from an element (which is always required) but also optionally the following:
How to compare keys.
A projection from original element to the element within a group.
A projection from a key and an enumeration of elements to a result type. If this is specified, the result is just a sequence of elements of this result type.
Table A.7. GroupBy
examples
Expression | Result |
---|---|
words.GroupBy(word => word.Length) | Key: 4; Sequence: "zero", "four" Key: 3; Sequence: "one", "two" Key: 5; Sequence: "three" |
words.GroupBy (word => word.Length, // Key word => word.ToUpper() // Group element ) | Key: 4; Sequence: "ZERO", "FOUR" Key: 3; Sequence: "ONE", "TWO" Key: 5; Sequence: "THREE" |
Frankly the last option is very confusing. I’d recommend avoiding it unless it definitely makes the code simpler for some reason.
Two operators are specified as join operators: Join
and GroupJoin
, both of which we saw in section 11.5 using join
and join
... into
query expression clauses respectively. Each method takes several parameters: two sequences, a key selector for each sequence, a projection to apply to each matching pair of elements, and optionally a key comparison.
For Join
the projection takes one element from each sequence and produces a result; for GroupJoin
the projection takes an element from the left sequence (in the chapter 11 terminology—the first one specified, usually as the sequence the extension method appears to be called on) and a sequence of matching elements from the right sequence. Both use deferred execution, and stream the left sequence but buffer the right sequence.
For the join examples in table A.8, we’ll match a sequence of names (Robin, Ruth, Bob, Emma) against a sequence of colors (Red, Blue, Beige, Green) by looking at the first character of both the name and the color, so Robin will join with Red and Bob will join with both Blue and Beige, for example.
Table A.8. Join
examples
Expression | Result |
---|---|
names.Join // Left sequence (colors, // Right sequence name => name[0], // Left key selector color => color[0], // Right key selector // Projection for result pairs (name, color) => name+" - "+color ) | "Robin - Red", "Ruth - Red", "Bob - Blue", "Bob - Beige" |
names.GroupJoin (colors, name => name[0], color => color[0], // Projection for key/sequence pairs (name, matches) => name+": "+ string.Join("/", matches.ToArray()) ) | "Robin: Red", "Ruth: Red", "Bob: Blue/Beige", "Emma: " |
Note that Emma doesn’t match any of the colors—the name doesn’t appear at all in the results of the first example, but it does appear in the second, with an empty sequence of colors.
The partitioning operators either skip an initial part of the sequence, returning only the rest, or take only the initial part of a sequence, ignoring the rest. In each case you can either specify how many elements are in the first part of the sequence, or specify a condition—the first part of the sequence continues until the condition fails. After the condition fails for the first time, it isn’t tested again—it doesn’t matter whether later elements in the sequence match or not. All of the partitioning operators (see table A.9) use deferred execution.
We’ve seen both projection operators (Select
and SelectMany
) in chapter 11. Select
is a simple one-to-one projection from element to result. SelectMany
is used when there are multiple from
clauses in a query expression: each element in the original sequence is used to generate a new sequence. Both projection operators (see table A.10) use deferred execution.
Table A.10. Projection examples
Expression | Result |
---|---|
words.Select(word => word.Length) | 4, 3, 3, 5, 4 |
words.Select ((word, index) => index.ToString()+": "+word) | "0: zero", "1: one", "2: two", "3: three", "4: four" |
words.SelectMany (word => word.ToCharArray()) | 'z', 'e', 'r', 'o', 'o', 'n', 'e', 't', 'w', 'o', 't', 'h', 'r', 'e', 'e', 'f', 'o', 'u', 'r' |
words.SelectMany ((word, index) => Enumerable.Repeat(word, index)) | "one", "two", "two", "three", "three", "three", "four", "four", "four", "four" |
There are overloads we didn’t see in chapter 11. Both methods have overloads that allow the index within the original sequence to be used within the projection, and SelectMany
either flattens all of the generated sequences into a single sequence without including the original element at all, or uses a projection to generate a result element for each pair of elements. Multiple from
clauses always use the overload that takes a projection. (Examples of this are quite long-winded, and not included here. See chapter 11 for more details.)
The quantifier operators (see table A.11) all return a Boolean value, using immediate execution:
All
checks whether all the elements in the sequence satisfy a specified condition.
Any
checks whether any of the elements in the sequence satisfy a specified condition, or if no condition is specified, whether there are any elements at all.
Contains
checks whether the sequence contains a particular element, optionally specifying a comparison to use.
Table A.11. Quantifier examples
Result | |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
words.Contains("FOUR", StringComparer.OrdinalIgnoreCase) |
|
The two filtering operators are OfType
and Where
. For details and examples of the OfType
operator, see the conversion operators section (A.3). The Where
operator (see table A.12) has overloads so that the filter can take account of the element’s index. It’s unusual to require the index, and the where
clause in query expressions doesn’t use this overload. Where
always uses deferred execution.
Table A.12. Filtering examples
Expression | Result |
---|---|
words.Where(word => word.Length > 3) words.Where ((word, index) => index < word.Length) | "zero", "three", "four" "zero", //length=4, index=0 "one", //length=3, index=1 "two", //length=3, index=2 "three", //length=5, index=3 // Not "four", length=4, index=4 |
It’s natural to be able to consider two sequences as sets of elements. The four set-based operators all have two overloads, one using the default equality comparison for the element type, and one where the comparison is specified in an extra parameter. All of them use deferred execution.
The Distinct
operator is the simplest—it acts on a single sequence, and just returns a new sequence of all the distinct elements, discarding duplicates. The other operators also make sure they only return distinct values, but they act on two sequences:
Intersect
returns elements that appear in both sequences.
Union
returns the elements that are in either sequence.
Except
returns the elements that are in the first sequence but not in the second. (Elements that are in the second sequence but not the first are not returned.)
For the examples of these operators in table A.13, we’ll use two new sequences: abbc ("a", "b", "b", "c")
and cd ("c", "d")
.
We’ve seen all the sorting operators before: OrderBy
and OrderByDescending
provide a “primary” ordering, while ThenBy
and ThenByDescending
provide subsequent orderings for elements that aren’t differentiated by the primary one. In each case a projection is specified from an element to its sorting key, and a comparison (between keys) can also be specified. Unlike some other sorting algorithms in the framework (such as List<T>.Sort
), the LINQ orderings are stable—in other words, if two elements are regarded as equal in terms of their sorting key, they will be returned in the order they appeared in the original sequence.
The final sorting operator is Reverse
, which simply reverses the order of the sequence. All of the sorting operators (see table A.14) use deferred execution, but buffer their data.
Table A.14. Sorting examples
Expression | Result |
---|---|
words.OrderBy(word => word) | "four", "one", "three", "two", "zero" |
// Order words by second character words.OrderBy(word => word[1]) | "zero", "three", "one", "four", "two" |
// Order words by length; // equal lengths returned in original // order words.OrderBy(word => word.Length) | "one", "two", "zero", "four", "three" |
words.OrderByDescending (word => word.Length) | "three", "zero", "four", "one", "two" |
// Order words by length and then // alphabetically words.OrderBy(word => word.Length) .ThenBy(word => word) | "one", "two", "four", "zero", "three" |
// Order words by length and then // alphabetically backwards words.OrderBy(word => word.Length) .ThenByDescending(word => word) | "two", "one", "zero", "four", "three" |
words.Reverse() | "four", "three", "two", "one", "zero" |
18.119.159.178