CHAPTER 3
This chapter discusses the standard query operators and is organized by the classification of the operators. Query operators can be grouped into categories such as join operators, generation operators, and projection operators.
At a higher level, query operators can be classified as falling into one of three categories based on their inputs/outputs:
· Input sequence(s) result in an output sequence.
· Input sequence results in scalar value/single element output.
· No input results in an output sequence (these operators generate their own elements).
The final category might seem strange at first glance, as these operators do not take an input sequence. These operators can be useful, for example, when sequence of integers needs creating, by reducing the amount of code we need to write to perform this generation.
Beyond this simple categorization based on input/output, the standard query operators can be grouped as shown in the following list. The operators in each category will be discussed in more detail later in this chapter.
Query operators:
· Restriction: Where
· Projection: Select, SelectMany
· Partitioning: Take, Skip, TakeWhile, SkipWhile
· Ordering: OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse
· Grouping: GroupBy
· Set: Concat, Union, Intersect, Except
· Conversion: ToArray, ToList, ToDictionary, ToLookup, OfType, Cast
· Element: First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, ElementAt, ElementAtOrDefault, DefaultIfEmpty
· Generation: Empty, Range, Repeat
· Quantifiers: Any, All, Contains, SequenceEqual
· Aggregate: Count, LongCount, Sum, Min, Max, Average, Aggregate
· Joining: Join, GroupJoin, Zip
Restriction query operators take an input sequence and output a sequence of elements that are restricted (“filtered”) in some way. The elements that make up the output sequence are those that match the specified restriction. Individual output elements themselves are not modified or transformed.
The Where query operator returns output elements that match a given predicate.
Tip: A predicate is a function that returns a bool result of true if some condition is satisfied.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> query = ingredients .Where(x => x.Calories >= 200); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); } |
The preceding code shows the Where operator in use. Here the predicate x => x.Calories >= 200 will return true for every ingredient that is greater than or equal to 200 calories.
This code produces the following output:
Sugar
Butter
An overload of Where provides the positional index of the input element for use in the predicate.
In the preceding code, “Sugar” has an index of 0 and “Butter” has an index of 4. The following query produces the same output as the previous version.
IEnumerable<Ingredient> queryUsingIndex = ingredients .Where( (ingredient, index) => ingredient.Name == "Sugar" || index == 4); |
Notice the lambda expression is different. In this code, the predicate function will be of type Func<Ingredient, int, bool>; the int parameter provides the position of the element in the input sequence.
Query expression usage
The where keyword is used in query expression style code, and is followed by a Boolean expression, as shown in the following code.
IEnumerable<Ingredient> queryEx = from i in ingredients where i.Calories >= 200 select i; |
Projection query operators take an input sequence, transform each element in that sequence, and produce an output sequence of these transformed elements. In this way, the input sequence is projected into a different output sequence.
The Select query operator transforms each element in the input sequence to an element in the output sequence. There will be the same number of elements in the output sequence as there are in the input sequence.
The following query projects the input sequence of Ingredient elements into the output sequence of string elements. The lambda expression is describing the projection: taking each input Ingredient element and returning a string element.
|
IEnumerable<string> query = ingredients.Select(x => x.Name); |
We could create an output sequence of ints with the following code:
|
IEnumerable<int> queryNameLength = ingredients.Select(x => x.Name.Length); |
The projection can also result in new complex objects being produced in output. In the following code each input Ingredient is projected into a new instance of an IngredientNameAndLength object.
class IngredientNameAndLength { public string Name { get; set; } public int Length { get; set; } public override string ToString() { return Name + " - " + Length; } } … IEnumerable<IngredientNameAndLength> query = ingredients.Select(x => new IngredientNameAndLength { Name = x.Name, Length = x.Name.Length }); |
This query could also be written using an anonymous type; notice in the following code the query return type has been change to var.
var query = ingredients.Select(x => new { Name = x.Name, Length = x.Name.Length }); |
Query expression usage
The select keyword is used in query expression style code as shown in the following code that replicates the preceding fluent style version using an anonymous type.
var query = from i in ingredients select new { Name = i.Name, Length = i.Name.Length }; |
Whereas the Select query operator returns an output element for every input element, the SelectMany query operator produces a variable number of output elements for each input element. This means that the output sequence may contain more or fewer elements than were in the input sequence.
The lambda expression in the Select query operator returns a single item. The lambda expression in a SelectMany query operator produces a child sequence. This child sequence may contain a varying number of elements for each element in the input sequence.
In the following code, the lambda expression will produce a child sequence of chars, one char for each letter of each input element. For example. the input element “Sugar,” when processed by the lambda expression, will produce a child sequence containing five elements: ‘S’, ‘u’, ‘g’, ‘a’, and ‘r’. Each input ingredient string can be a different number of letters, so the lambda expression will produce child sequences of different lengths. Each of the child sequences produced are concatenated (“flattened”) into a single output sequence.
string[] ingredients = {"Sugar", "Egg", "Milk", "Flour", "Butter"}; IEnumerable<char> query = ingredients.SelectMany(x => x.ToCharArray()); foreach (char item in query) { Console.WriteLine(item); } |
This code produces the following output (notice there are more output elements than there were in the input sequence that only contained five elements):
S
u
g
a
r
E
g
g
M
i
l
k
F
l
o
u
r
B
u
t
t
e
r
Query expression usage
In the query expression style, an additional from clause is added to produce the child sequence.
The following code produces the same output as the previous fluent version.
IEnumerable<char> query = from i in ingredients from c in i.ToCharArray() select c; |
The partitioning query operators take an input sequence and partition it, or “return a chunk” in the output sequence. One use of these operators is to break up a larger sequence into “pages,” for example, paging though ten results at a time in a user interface.
The Take query operator takes in an input sequence and returns the specified number of elements as an output.
The following code shows how to use the Take query operator to return the first three ingredients in the sequence.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> firstThree = ingredients.Take(3);
foreach (var ingredient in firstThree) { Console.WriteLine(ingredient.Name); } |
The preceding code produces the following output:
Sugar
Egg
Milk
As with other query operators, Take can be chained together. In the following code, Where is used to restrict the ingredients to only those containing more than 100 calories. The Take query operator then takes this restricted sequence and returns the first two ingredients.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> query = ingredients .Where(x => x.Calories > 100) .Take(2); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); } |
This code produces the following result:
Sugar
Milk
Query expression usage
There is no keyword for Take when using the query expression style; however, mixed style can be used as the following code demonstrates.
IEnumerable<Ingredient> query = (from i in ingredients where i.Calories > 100 select i).Take(2); |
The TakeWhile query operator, rather than return elements based on a fixed specified number of elements, instead continues to return input elements until such time as a specified predicate becomes false.
The following code will continue to “take” elements from the input sequence while the number of calories is greater than or equal to 100.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> query = ingredients .TakeWhile(x => x.Calories >= 100); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); } |
This produces the following output:
Sugar
Egg
Milk
Notice here that as soon as an element in the input sequence is reached that does not match the predicate, the “taking” stops; no more elements from the remainder of the input sequence are returned, even if they match the predicate.
There is an overload of TakeWhile that allows the positional index of the input element to be used; in the preceding code, this predicate would take the form Func<Ingredient, int, bool>. The int generic type parameter here represents the positional index of the current Ingredient being examined.
Query expression usage
There is no keyword for TakeWhile when using the query expression style; however, mixed style can be used.
The Skip query operator will ignore the specified number of input elements from the start of the input sequence and return the remainder.
The following code skips over the first three elements and returns the rest:
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> query = ingredients.Skip(3); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); } |
This produces the following output:
Flour
Butter
Query expression usage
There is no keyword for Skip when using the query expression style; however, mixed style can be used.
Using Skip and Take for result set paging
When used together, the Skip and Take query operators can implement the paging of result sets for display to users, as demonstrated in the following code.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> firstPage = ingredients.Take(2); IEnumerable<Ingredient> secondPage = ingredients .Skip(2) .Take(2); IEnumerable<Ingredient> thirdPage = ingredients .Skip(4) .Take(2); Console.WriteLine("Page One:"); foreach (var ingredient in firstPage) { Console.WriteLine(" - " + ingredient.Name); } Console.WriteLine("Page Two:"); foreach (var ingredient in secondPage) { Console.WriteLine(" - " + ingredient.Name); } Console.WriteLine("Page Three:"); foreach (var ingredient in thirdPage) { Console.WriteLine(" - " + ingredient.Name); } |
This produces the following output:
Page One:
- Sugar
- Egg
Page Two:
- Milk
- Flour
Page Three:
- Butter
Like TakeWhile, the SkipWhile query operator uses a predicate to evaluate each element in the input sequence. SkipWhile will ignore items in the input sequence until the supplied predicate returns false.
The following code will skip input elements until “Milk” is encountered; at this point the predicate x => x.Name != "Milk" becomes false, and the remainder of the ingredients will be returned.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; IEnumerable<Ingredient> query = ingredients.SkipWhile(x => x.Name != "Milk"); foreach (var ingredient in query) { Console.WriteLine(ingredient.Name); } |
This produces the following output:
Milk
Flour
Butter
Just as with TakeWhile, there is an overload of SkipWhile that allows the positional index of the input element to be used.
Query expression usage
There is no keyword for SkipWhile when using the query expression style; however, mixed style can be used.
The ordering query operators return the same number of elements as the input sequence, but arranged in a (potentially) different order.
The OrderBy query operator sorts the input sequence by comparing the elements in the input sequence using a specified key.
The following code shows the sorting of a simple input sequence of strings. In this example, the key is the string itself as specified by the lambda expression x => x.
string[] ingredients = { "Sugar", "Egg", "Milk", "Flour", "Butter" }; var query = ingredients.OrderBy(x => x);
foreach (var item in query) { Console.WriteLine(item); } |
This produces the following output:
Butter
Egg
Flour
Milk
Sugar
The lambda expression that is used to specify the sorting key can specify a property of the input element itself, as the following code example shows.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 200} }; var query = ingredients.OrderBy(x => x.Calories); foreach (var item in query) { Console.WriteLine(item.Name + " " + item.Calories); } |
In the preceding code, the lambda expression x => x.Calories is selecting the Calories property to be the key that is sorted on.
This produces the following output:
Flour 50
Egg 100
Milk 150
Butter 200
Sugar 500
Query expression usage
The orderby keyword is used when using the query expression style, as shown in the following code.
var query = from i in ingredients orderby i.Calories select i; |
The ThenBy query operator can be chained one or multiple times following an initial OrderyBy. The ThenBy operator adds additional levels of sorting. In the following code, the ingredients are first sorted with the initial OrderBy sort (sorting into calorie order); this sorted sequence is then further sorted by ingredient name while maintaining the initial calorie sort.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Milk", Calories = 100}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Flour", Calories = 500}, new Ingredient {Name = "Butter", Calories = 200} }; var query = ingredients.OrderBy(x => x.Calories) .ThenBy(x => x.Name); foreach (var item in query) { Console.WriteLine(item.Name + " " + item.Calories); } |
This produces the following output:
Egg 100
Milk 100
Butter 200
Flour 500
Sugar 500
Notice that in the original input sequence, “Milk” came before “Egg,” but because of the ThenBy operator, in the output the ingredients are sorted by name within matching calorie values.
Query expression usage
The orderby keyword is used when using the query expression style, but rather than a single sort expression, subsequent sort expressions are added as a comma-separated list, as shown in the following code.
var query = from i in ingredients orderby i.Calories, i.Name select i; |
The OrderByDescending query operator works in the same fashion as the OrderBy operator, except that the results are returned in the reverse order, as the following code demonstrates.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Milk", Calories = 100}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Flour", Calories = 500}, new Ingredient {Name = "Butter", Calories = 200} }; var query = ingredients.OrderByDescending(x => x.Calories); foreach (var item in query) { Console.WriteLine(item.Name + " " + item.Calories); } |
This produces the following output:
Sugar 500
Flour 500
Butter 200
Milk 100
Egg 100
Note that this time the calorie numbers are in descending order.
Query expression usage
The descending keyword is used when using the query expression style, and is applied after the sort expression itself, as shown in the following code.
var query = from i in ingredients orderby i.Calories descending select i; |
The ThenByDescending query operator follows the same usage as the ThenBy operator, but returns results in the reverse sort order.
The following code shows the ingredients being sorted first by calorie (in ascending order), then by the ingredient names, but this time the names are sorted in descended order.
Ingredient[] ingredients = { new Ingredient {Name = "Flour", Calories = 500}, new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 100}, new Ingredient {Name = "Butter", Calories = 200} }; var query = ingredients.OrderBy(x => x.Calories) .ThenByDescending(x => x.Name); foreach (var item in query) { Console.WriteLine(item.Name + " " + item.Calories); } |
This produces the following output:
Milk 100
Egg 100
Butter 200
Sugar 500
Flour 500
Notice here that in the input sequence, “Flour” comes before “Sugar,” but because of the ThenByDescending, they are reversed in the output.
Query expression usage
The descending keyword is used when using the query expression style, as shown in the following code.
var query = from i in ingredients orderby i.Calories, i.Name descending select i; |
The Reverse query operator simply takes the input sequence and returns the elements in the reverse order in the output sequence; so, for example, the first element in the input sequence will become the last element in the output sequence.
char[] letters ={'A', 'B', 'C'}; var query = letters.Reverse(); foreach (var item in query) { Console.WriteLine(item); } |
The preceding code produces the following output:
C
B
A
Query expression usage
There is no keyword for Reverse when using the query expression style; however, mixed style can be used.
The GroupBy query operator takes an input sequence and returns an output sequence of sequences (groups). The basic overload to the GroupBy method takes a lambda expression representing the key to create the groups for.
The following code shows how to group all ingredients by number of calories.
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Lard", Calories=500}, new Ingredient{Name = "Butter", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=100}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Oats", Calories=50}
}; IEnumerable<IGrouping<int, Ingredient>> query = ingredients.GroupBy(x => x.Calories); foreach (IGrouping<int, Ingredient> group in query) { Console.WriteLine("Ingredients with {0} calories", group.Key); foreach (Ingredient ingredient in group) { Console.WriteLine(" - {0}", ingredient.Name); } } |
Notice in the preceding code the output sequence of sequences is essentially a list (IEnumerable) of IGrouping. Each IGrouping element contains the key (in this case an int for the calories), and the list of ingredients that have the same number of calories.
This produces the following output:
Ingredients with 500 calories
- Sugar
- Lard
- Butter
Ingredients with 100 calories
- Egg
- Milk
Ingredients with 50 calories
- Flour
- Oats
Query expression usage
The group keyword is used when using the query expression style; we saw the group keyword in use in Chapter 2, “Fluent and Query Expression Styles.”
The set query operators perform set-based operations. These operators take two input sequences representing the two “sets” and return a single output sequence.
The set query operators consist of the following:
· Concat
· Union
· Distinct
· Intersect
· Except
The Concat query operator takes a first sequence and returns this with all the elements of a second sequence added to it, as shown in the following code.
string[] applePie = {"Apple", "Sugar", "Pastry", "Cinnamon"}; string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" }; IEnumerable<string> query = applePie.Concat(cherryPie); foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Apple
Sugar
Pastry
Cinnamon
Cherry
Sugar
Pastry
Kirsch
Notice that any duplicate elements (such as “Sugar” and “Pastry”) are preserved.
Query expression usage
There is no keyword for Concat when using the query expression style; however, mixed style can be used.
The Union query operator performs a similar “joining” operation as Concat; however, any duplicate elements that exist in both the first and second sequence are not duplicated in the output.
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" }; string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" }; IEnumerable<string> query = applePie.Union(cherryPie); foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Apple
Sugar
Pastry
Cinnamon
Cherry
Kirsch
Notice here that any duplicates (e.g. “Sugar” and “Pastry”) have been removed from the output sequence.
Query expression usage
There is no keyword for Union when using the query expression style; however, mixed style can be used.
While the Distinct query operator may not, strictly speaking, be a set operator in that it only operates on a single input sequence, it is included here to show how the Union operator can by simulated by using a Concat followed by a Distinct. Also note that the Distinct query operator may be chained together with other query operators and not just the set-based operators.
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" }; string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" }; IEnumerable<string> query = applePie.Concat(cherryPie) .Distinct(); foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Apple
Sugar
Pastry
Cinnamon
Cherry
Kirsch
Notice this is that same output as when using Union.
Query expression usage
There is no keyword for Distinct when using the query expression style; however, mixed style can be used.
The Intersect query operator returns only those elements that exist in both the first and second input sequences. In the following code, the only two elements that are common to both sequences are “Sugar” and “Pastry.”
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" }; string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" }; IEnumerable<string> query = applePie.Intersect(cherryPie);
foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Sugar
Pastry
Also notice here that the common elements are not duplicated in the output sequence; we do not see two “Sugar” elements, for example.
Query expression usage
There is no keyword for Intersect when using the query expression style; however, mixed style can be used.
The Except query operator will return only those elements in the first sequence where those same elements do not exist in the second sequence.
Take the following code:
string[] applePie = { "Apple", "Sugar", "Pastry", "Cinnamon" }; string[] cherryPie = { "Cherry", "Sugar", "Pastry", "Kirsch" }; IEnumerable<string> query = applePie.Except(cherryPie); foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Apple
Cinnamon
Note that we do not get any elements returned from the second sequence.
Query expression usage
There is no keyword for Except when using the query expression style; however, mixed style can be used.
The conversion query operators convert from IEnumerable<T> sequences to other types of collections.
The following query operators can be used to perform conversion of sequences:
· OfType
· Cast
· ToArray
· ToList
· ToDictionary
· ToLookup
The OfType query operator returns an output sequence containing only those elements of a specified type. The extension method that implements the OfType query operator has the following signature:
public static IEnumerable<TResult> OfType<TResult>(this IEnumerable source) |
Notice here that the extension method is “extending” the non-generic type IEnumerable. This means that the input sequence may contain elements of different types.
The following code shows an IEnumerable (object array) containing objects of different types. The OfType query operator can be used here to return (for example) all the string objects. Also notice in the following code that the result of the query is the generic version of IEnumerable, namely IEnumerable<string>.
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime() }; IEnumerable<string> query = input.OfType<string>(); foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Apple
Sugar
Because IEnumerable<T> implements IEnumerable, OfType can also be used on strongly-typed sequences. The following code show a simple object-oriented hierarchy.
class Ingredient { public string Name { get; set; } } class DryIngredient : Ingredient { public int Grams { get; set; } } class WetIngredient : Ingredient { public int Millilitres { get; set; } } |
The following code shows how OfType can be used to get a specific subtype, in this example getting all the WetIngredient objects:
IEnumerable<Ingredient> input = new Ingredient[] { new DryIngredient{ Name = "Flour"}, new WetIngredient{Name = "Milk"}, new WetIngredient{Name = "Water"} }; IEnumerable<WetIngredient> query = input.OfType<WetIngredient>(); foreach (WetIngredient item in query) { Console.WriteLine(item.Name); } |
This produces the following output:
Milk
Water
Query expression usage
There is no keyword for OfType when using the query expression style; however, mixed style can be used.
Like the OfType query operator, the Cast query operator can take an input sequence and return an output sequence containing elements of a specific type. Each element in the input sequence is cast to the specified type; however, unlike with OfType, which just ignores any incompatible types, if the cast is not successful, an exception will be thrown.
The following code shows an example of where using Cast will throw an exception.
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime() }; IEnumerable<string> query = input.Cast<string>(); foreach (string item in query) { Console.WriteLine(item); } |
Running this code will cause a “System.InvalidCastException : Unable to cast object of type 'System.Int32' to type 'System.String'” exception.
Query expression usage
While there is no keyword for Cast when using the query expression style, it is supported by giving the range variable a specific type, as in the following code.
IEnumerable input = new object[] {"Apple", "Sugar", "Flour"}; IEnumerable<string> query = from string i in input select i; |
Note in the preceding code the explicit type declaration string before the range variable name i.
The ToArray query operator takes an input sequence and creates an output array containing the elements. ToArray will also cause immediate query execution (enumeration of the input sequence) and overrides the default deferred execution behavior of LINQ.
In the following code, the IEnumerable<string> is converted to an array of strings.
IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" }; string[] array = input.ToArray(); |
Query expression usage
There is no keyword for ToArray when using the query expression style; however, mixed style can be used.
The ToList query operator, like the ToArray operator, usually bypasses deferred execution. Whereas ToArray creates an array representing the input elements, ToList, as its name suggests, instead outputs a List<T>.
IEnumerable<string> input = new []{ "Apple", "Sugar", "Flour" }; List<string> list = input.ToList(); |
Query expression usage
There is no keyword for ToList when using the query expression style; however, mixed style can be used.
The ToDictionary query operator converts an input sequence into a generic Dictionary<TKey, TValue>.
The simplest overload of the ToDictionary method takes a lambda expression representing a function, which selects what is used for the key for each item in the resulting Dictionary.
IEnumerable<Recipe> recipes = new[] { new Recipe {Id = 1, Name = "Apple Pie", Rating = 5}, new Recipe {Id = 2, Name = "Cherry Pie", Rating = 2}, new Recipe {Id = 3, Name = "Beef Pie", Rating = 3} };
Dictionary<int, Recipe> dict = recipes.ToDictionary(x => x.Id); foreach (KeyValuePair<int, Recipe> item in dict) { Console.WriteLine("Key = {0}, Recipe = {1}", item.Key, item.Value.Name); } |
In the preceding code, the Key of the items in the resultant Dictionary is of type int and is set to the Id each Recipe. The Value of each item in the Dictionary represents the Recipe itself.
Running the preceding code produces the following output:
Key = 1, Recipe = Apple Pie
Key = 2, Recipe = Cherry Pie
Key = 3, Recipe = Beef Pie
Query expression usage
There is no keyword for ToDictionary when using the query expression style; however, mixed style can be used.
The ToLookup query operator works in a similar fashion to the ToDictionary operator, but rather than producing a dictionary, it creates an instance of an ILookUp.
The following code shows the creation of a lookup that groups recipes into similar ratings. In this example, the key comes from the byte rating.
IEnumerable<Recipe> recipes = new[] { new Recipe {Id = 1, Name = "Apple Pie", Rating = 5}, new Recipe {Id = 1, Name = "Banana Pie", Rating = 5}, new Recipe {Id = 2, Name = "Cherry Pie", Rating = 2}, new Recipe {Id = 3, Name = "Beef Pie", Rating = 3} }; ILookup<byte, Recipe> look = recipes.ToLookup(x => x.Rating); foreach (IGrouping<byte, Recipe> ratingGroup in look) { byte rating = ratingGroup.Key; Console.WriteLine("Rating {0}", rating); foreach (var recipe in ratingGroup) { Console.WriteLine(" - {0}", recipe.Name); } } |
This produces the following output:
Rating 5
- Apple Pie
- Banana Pie
Rating 2
- Cherry Pie
Rating 3
- Beef Pie
Query expression usage
There is no keyword for ToLookup when using the query expression style; however, mixed style can be used.
The element operators take an input sequence and return a single element from that input sequence, or some other default single value. In this way, the element operators do not themselves return sequences.
The First query operator simply returns the initial element in the input sequence, as the following code demonstrates.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(); Console.WriteLine(element.Name); |
This produces the following output:
Sugar
An overload of First allows a predicate to be specified. This will return the first item in the input sequence that satisfies this predicate. The following code shows how to find the first Ingredient element that has a calorie value of 150.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(x => x.Calories == 150); Console.WriteLine(element.Name); |
This produces the following output:
Milk
If the input sequence contains zero elements, the use of First will result in an exception.
The following code will result in an exception: “System.InvalidOperationException : Sequence contains no elements”.
Ingredient[] ingredients = {}; Ingredient element = ingredients.First(); |
If First is used with a predicate and that predicate cannot be satisfied by any element in the input sequence, an exception will also be thrown. The following code produces the exception: “System.InvalidOperationException : Sequence contains no matching element.” This is because no Ingredient exists with 9,999 calories.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(x => x.Calories == 9999); |
Query expression usage
There is no keyword for First when using the query expression style; however, mixed style can be used.
The FirstOrDefault query operator works in a similar way to First, but instead of an exception being thrown, the default value for the element type is returned. This default value will be null for reference types, zero for numeric types, and false for Boolean types.
The following code uses FirstOrDefault with an empty sequence:
Ingredient[] ingredients = { }; Ingredient element = ingredients.FirstOrDefault(); Console.WriteLine(element == null); |
This produces the following output:
True
Notice that no exception is thrown, and the resulting element is set to null. The same behavior is true for the version of FirstOrDefault that takes a predicate, as the following example shows.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.FirstOrDefault(x => x.Calories == 9999); Console.WriteLine(element == null); |
This produces the following output:
True
Query expression usage
There is no keyword for FirstOrDefault when using the query expression style; however, mixed style can be used.
The Last query operator returns the final element in the input sequence. As with First, there is an overloaded version that also takes a predicate. This predicate version will return the last element in the sequence that satisfies the predicate. Also as with First, an exception will be thrown if the input sequence contains zero elements, or if the predicate cannot be satisfied by any element.
The following code shows the predicate version of Last.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.Last(x => x.Calories == 50); Console.WriteLine(element.Name); |
This produces the following output:
Flour
Notice here that “Flour” is not the last element in the sequence as a whole, but rather the last element that has 50 calories.
Query expression usage
There is no keyword for Last when using the query expression style; however, mixed style can be used.
The LastOrDefault query operator works in a similar way to Last, and also has an overloaded version that takes a predicate.
Like FirstOrDefault, if the input sequence is empty or no element satisfies the predicate, rather than an exception being thrown, the default value for the element type is returned instead.
Query expression usage
There is no keyword for LastOrDefault when using the query expression style; however, mixed style can be used.
The Single query operator returns the only element in the input sequence. If the input sequence contains more than one element, an exception is thrown: ”System.InvalidOperationException : Sequence contains more than one element.” If the input sequence contains zero elements, an exception will also be thrown: “System.InvalidOperationException : Sequence contains no elements.”
The following code uses Single to retrieve the only element in the input sequence.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500} }; Ingredient element = ingredients.Single(); Console.WriteLine(element.Name); |
This produces the following output:
Sugar
There is an overload of Single that allows a predicate to be specified. The predicate will locate and return the single element that matches it. If there is more than one element in the input sequence that matches the predicate, then an exception will be thrown: “System.InvalidOperationException : Sequence contains more than one matching element.”
The following code shows how to use the predicate version of Single. Even though there is more than one element in the input sequence, because there is not more than one element that matches the predicate, an exception will not be thrown.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Butter", Calories = 150}, new Ingredient {Name = "Milk", Calories = 500} }; Ingredient element = ingredients.Single(x => x.Calories == 150); Console.WriteLine(element.Name); |
This produces the following output:
Butter
If the predicate is changed (as in the following code) so that it matches more than one element, then an exception will be thrown.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Butter", Calories = 150}, new Ingredient {Name = "Milk", Calories = 500} }; Ingredient element = ingredients.Single(x => x.Calories == 500); |
Notice the predicate in the preceding code will find two ingredients with calories of 500, there will be an exception.
Query expression usage
There is no keyword for Single when using the query expression style; however, mixed style can be used.
The SingleOrDefault query operator works in a similar fashion to Single. However, rather than throwing an exception when there are zero input elements, the default for the type is returned. If the predicate version is being used and no matching elements are found, the default value is again returned, rather than an exception (as happens with Single). To summarize, for SingleOrDefault to not throw an exception, either zero or one element must be found. Zero found elements will result in a default value, one element found will be returned, more than one element found will result in an exception.
The following code shows the predicate version of SingleOrDefault being used where there will be zero matching elements found for 9,999 calories.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.SingleOrDefault(x => x.Calories == 9999); Console.WriteLine(element == null); |
This produces the following output:
True
Note that no exception is thrown, rather the default value (null) is returned.
Query expression usage
There is no keyword for SingleOrDefault when using the query expression style; however, mixed style can be used.
The ElementAt query operator returns the element that exists at the specified position in the input sequence.
The following code selects the third element from the input sequence.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.ElementAt(2); Console.WriteLine(element.Name); |
This produces the following output:
Milk
Notice that the value passed to ElementAt is a zero-based index; for example, to select the first element, the value would be 0.
If the value passed to ElementAt tries to select an element that is greater than the number of elements in the input sequence, an exception will be thrown: “Index was out of range. Must be non-negative and less than the size of the collection.”
Query expression usage
There is no keyword for ElementAt when using the query expression style; however, mixed style can be used.
The ElementAtOrDefault query operator works in a similar way to ElementAt, but rather than throw an exception if the value passed to it exceeds the size of the input sequence, it will return the default value for the input type. The following code demonstrates this.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.ElementAtOrDefault(4); Console.WriteLine(element == null); |
This produces the following output:
True
Query expression usage
There is no keyword for ElementAtOrDefault when using the query expression style; however, mixed style can be used.
The DefaultIfEmpty query operator takes an input sequence and does one of two things. If the input sequence contains at least one element, then the input sequence is returned without any changes. If, however, the input sequence contains zero elements, the output sequence returned will not be empty; it will contain a single element with a default value.
The following code shows what happens when the input sequence contains elements.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty(); foreach (Ingredient item in query) { Console.WriteLine(item.Name); } |
This produces the following output:
Sugar
Egg
Milk
Note this is exactly the same as the input sequence.
The following code shows the second mode of operation where the input sequence contains zero elements.
Ingredient[] ingredients = {}; IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty(); foreach (Ingredient item in query) { Console.WriteLine(item == null); } |
This produces the following output:
True
Notice here that the input sequence was empty, but the output sequence contains a single null element, a null being the default value for Ingredient (which is a reference type).
Query expression usage
There is no keyword for DefaultIfEmpty when using the query expression style; however, mixed style can be used.
The generation query operators create sequences for us.
The generation operators differ from the majority of the other standard query operators in two main ways. The first is that they do not take an input sequence; the second is that they are not implemented as extension methods, but rather as plain static methods of the Enumerable class.
As an example, the following code shows the signature of the Empty query operator.
public static IEnumerable<TResult> Empty<TResult>() |
The Empty query operator creates an empty sequence (zero elements) of a specified type.
The following code shows the creation of empty sequence of Ingredient. Notice that the type of the empty sequence we want is specified as a generic type parameter of the Empty method.
IEnumerable<Ingredient> ingredients = Enumerable.Empty<Ingredient>(); Console.WriteLine(ingredients.Count()); |
This produces the following output:
0
Query expression usage
There is no keyword for Empty when using the query expression style; however, mixed style can be used.
The Range query operator creates a sequence of integer values. When using Range, the first parameter specified is the starting number of the range to be generated. The second parameter is the total number of integer elements to generate, starting at the first parameter value.
IEnumerable<int> fiveToTen = Enumerable.Range(5, 6); foreach (int num in fiveToTen) { Console.WriteLine(num); } |
This produces the following output:
5
6
7
8
9
10
Notice that a total of six elements have been generated, as specified by the second parameter in the call to Range.
Query expression usage
There is no keyword for Range when using the query expression style; however, mixed style can be used.
The Repeat query operator repeats a specified integer a specified number of times, as the following code shows.
IEnumerable<int> nums = Enumerable.Repeat(42, 5); foreach (int num in nums) { Console.WriteLine(num); } |
This produces the following output:
42
42
42
42
42
Query expression usage
There is no keyword for Repeat when using the query expression style; however, mixed style can be used.
The quantifier query operators take an input sequence (or in the case of SequenceEqual, two input sequences), evaluate it, and return a single Boolean result.
The Contains query operator evaluates the elements in the input sequence and returns true if a specified value exists.
int[] nums = {1, 2, 3}; bool isTwoThere = nums.Contains(2); bool isFiveThere = nums.Contains(5); Console.WriteLine(isTwoThere); Console.WriteLine(isFiveThere); |
This produces the following output:
True
False
Query expression usage
There is no keyword for Contains when using the query expression style; however, mixed style can be used.
There are two overloads of the Any query operator. The first version simply returns true if the input sequence contains at least one element. The second version of Any takes a predicate as a parameter and returns true if at least one element in the input sequence satisfies the predicate.
The following code demonstrates the simple version of Any.
int[] nums = { 1, 2, 3 }; IEnumerable<int> noNums = Enumerable.Empty<int>();
Console.WriteLine( nums.Any() ); Console.WriteLine( noNums.Any() ); |
This produces the following output:
True
False
The following code shows the predicate overload of Any to determine if a sequence contains any even numbers.
int[] nums = { 1, 2, 3 }; bool areAnyEvenNumbers = nums.Any(x => x % 2 == 0); Console.WriteLine(areAnyEvenNumbers); |
This produces the following output:
True
Query expression usage
There is no keyword for Any when using the query expression style; however, mixed style can be used.
The All query operator takes a predicate and evaluates the elements in the input sequence to determine if every element satisfies this predicate.
The following code checks that a sequence of ingredients constitutes a low-fat recipe.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 400} }; bool isLowFatRecipe = ingredients.All(x => x.Calories < 200); Console.WriteLine(isLowFatRecipe); |
This produces the following output:
False
Note: As soon as an element is found that does not satisfy the predicate, All() returns false and does not examine any subsequent elements.
Query expression usage
There is no keyword for All when using the query expression style; however, mixed style can be used.
The SequenceEqual query operator compares two sequences to see if they both have exactly the same elements in the same order.
The following code compares two identical sequences:
IEnumerable<int> sequence1 = new[] {1, 2, 3}; IEnumerable<int> sequence2 = new[] {1, 2, 3}; bool isSeqEqual = sequence1.SequenceEqual(sequence2); Console.WriteLine(isSeqEqual); |
This produces the following output:
True
If the two sequences being compared contain the same elements, but in different orders, then SequenceEqual will return false, as the following code demonstrates.
IEnumerable<int> sequence1 = new[] { 1, 2, 3 }; IEnumerable<int> sequence2 = new[] { 3, 2, 1 }; bool isSeqEqual = sequence1.SequenceEqual(sequence2); Console.WriteLine(isSeqEqual); |
This produces the following output:
False
Notice here that even though the sequences contain the same values (1, 2, 3) the order is different, so SequenceEqual returns false.
Query expression usage
There is no keyword for SequenceEqual when using the query expression style; however, mixed style can be used.
The aggregate operators take an input sequence and return a single scalar value. The returned value is not one of the elements from the input sequence, but rather some derived value computed from the elements in the input sequence.
The aggregate query operators are:
· Count
· LongCount
· Sum
· Min
· Max
· Average
· Aggregate
The Count query operator simply returns the number of elements contained in the input sequence. There are two overloads, one of which accepts a predicate.
The following example shows the non-predicate version.
int[] nums = {1, 2, 3}; int numberOfElements = nums.Count(); Console.WriteLine(numberOfElements); |
This produces the following output:
3
When a predicate is supplied, the count is restricted to those elements satisfying the predicate. The following code shows a predicate being used to count all the even numbers in the input sequence.
int[] nums = { 1, 2, 3 }; int numberOfEvenElements = nums.Count(x => x % 2 == 0); Console.WriteLine(numberOfEvenElements); |
This produces the following output:
1
Tip: Using Any() instead of Count() != 0 usually conveys the intent of the code better, and in some circumstances, can perform better.
Query expression usage
There is no keyword for Count when using the query expression style; however, mixed style can be used.
The LongCount query operator provides the same features as Count (including a predicate overload), but instead of returning an int, it returns a long, so it can be used with very long input sequences.
Query expression usage
There is no keyword for LongCount when using the query expression style; however, mixed style can be used.
The Sum query operator adds together all the elements of the input sequence and returns the total result.
The following code demonstrates adding together a sequence of ints.
int[] nums = { 1, 2, 3 }; int total = nums.Sum(); Console.WriteLine(total); |
This produces the following output:
6
The Sum query operator is implemented as individual separate extension methods for IEnumerable<T> (for example, IEnumerable<long>) for each of the numeric types (and nullable equivalents). This means that in order to use Sum on non-numeric sequences, an overload must be used to select a numeric value from the non-numeric input elements. The following code shows how to Sum the calorie values of a sequence of Ingredient.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 400} }; int totalCalories = ingredients.Sum(x => x.Calories); Console.WriteLine(totalCalories); |
This produces the following output:
1200
Query expression usage
There is no keyword for Sum when using the query expression style; however, mixed style can be used.
The Average query operator works on a sequence of numeric values and calculates the average.
The following code shows Average in use.
int[] nums = { 1, 2, 3 }; var avg = nums.Average(); Console.WriteLine(avg); |
This produces the following output:
2
Notice in the preceding code the type for total has not been explicitly set; instead, var is being used. Depending on the numeric input type, Average will return different output numeric types. For example, the following code will not compile because the implementation of Average for IEnumerable<int> returns a double.
int[] nums = { 1, 2, 3 }; int avg = nums.Average(); // this line causes compilation failure |
Average will return either a double, float, or decimal return type (or the nullable versions of these.)
As with Sum, Average can be used on an input sequence of non-numeric values using an overload, which allows a numeric value to be selected, as shown in the following code.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 400} }; var avgCalories = ingredients.Average(x => x.Calories); Console.WriteLine(avgCalories); |
This produces the following output:
240
Query expression usage
There is no keyword for Average when using the query expression style; however, mixed style can be used.
The Min query operator returns the smallest element from the input sequence, as the following code demonstrates.
int[] nums = { 3, 2, 1 }; var smallest = nums.Min(); Console.WriteLine(smallest); |
This produces the following output:
1
An overload of Min also allows the selector to be specified, as the following code shows.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 400} }; var smallestCalories = ingredients.Min(x => x.Calories); Console.WriteLine(smallestCalories); |
This produces the following output:
50
Query expression usage
There is no keyword for Min when using the query expression style; however, mixed style can be used.
The Max query operator complements the Min operator and returns the smallest value from the input sequence, as the following code demonstrates.
int[] nums = { 1, 3, 2 }; var largest = nums.Max(); Console.WriteLine(largest); |
This produces the following output:
3
As with Min, there is an overload of Max that allows a selector to be specified.
Query expression usage
There is no keyword for Max when using the query expression style; however, mixed style can be used.
The previous aggregate operators all perform a specific aggregation; the Aggregate query operator is a more advanced operator that allows custom aggregations to be defined and executed against an input sequence.
There are two main versions of Aggregate: one that allows a starting “seed” value to be specified, and one that uses the first element in the sequence as the seed value.
All versions of Aggregate require an “accumulator function” to be specified. The accumulator, as its name suggests, accumulates a result as each element in the input sequence is processed. The accumulator function is passed the value of the current element being processed and the current accumulated value.
The following code demonstrates how to simulate the Sum operator using Aggregate.
int[] nums = { 1, 2, 3 }; var result = nums.Aggregate(0, (currentElement, runningTotal) => runningTotal + currentElement); Console.WriteLine(result); |
This produces the following output:
6
The preceding code uses an overload of Aggregate that specifies the initial seed value, in this example a seed of zero.
Query expression usage
There is no keyword for Aggregate when using the query expression style; however, mixed style can be used.
The joining query operators take two input sequences, combine them in some way, and output a single sequence.
The Join query operator takes an initial input sequence (the “outer sequence”), and to this outer sequence a second “inner sequence” is introduced. The output from the Join query operator is a flat (i.e. non-hierarchical) sequence.
The Join operator takes a number of parameters:
· IEnumerable<TInner> inner – the inner sequence.
· Func<TOuter, TKey> outerKeySelector – what key to join on in outer sequence elements.
· Func<TInner, TKey> innerKeySelector - what key to join on in inner sequence elements.
· Func<TOuter, TInner, TResult> resultSelector – What the output elements will look like
Note: There is also an overload that allows a specific IEqualityComparer to be used.
The following code shows the Join query operator in use. Note the explicit type definitions in the key selection lambdas; this is for demonstration code clarity. For example: (Recipe outerKey) => outerKey.Id could be simplified to outerKey=> outerKey.Id.
Recipe[] recipes = // outer sequence { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; Review[] reviews = // inner sequence { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes // recipes is the outer sequence .Join( reviews, // reviews is the inner sequence (Recipe outerKey) => outerKey.Id, // the outer key (Review innerKey) => innerKey.RecipeId, // the inner key // choose the shape ("projection") of resulting elements (recipe, review) => recipe.Name + " - " + review.ReviewText); foreach (string item in query) { Console.WriteLine(item); } |
This produces the following output:
Mashed Potato - Tasty!
Mashed Potato - Not nice :(
Mashed Potato - Pretty good
Crispy Duck - Too hard
Crispy Duck - Loved it
Notice in this output that “Sachertorte” does not appear. This is because Join performs a left join, so elements in the outer sequence that have no matches in the inner sequence will not be included in the output sequence.
Query expression usage
The join keyword is used when using the query expression style. The following code shows how to perform an inner join using the query expression style. See Chapter 2, “Fluent and Query Expression Styles for usage of the join keyword.
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId select new // anonymous type { RecipeName = recipe.Name, RecipeReview = review.ReviewText }; |
Tip: Consider using the query expression style when performing joins, as it is usually more readable.
The GroupJoin query operator joins an inner and outer sequence, just as with the Join query operator; however, GroupJoin produces a hierarchical result sequence. The output sequence is essentially a sequence of groups, with each group able to contain the sequence of items from the inner sequence that belong to that group.
The GroupJoin operator takes a number of parameters:
· IEnumerable<TInner> inner: The inner sequence.
· Func<TOuter, TKey> outerKeySelector: What key to join on in outer sequence elements.
· Func<TInner, TKey> innerKeySelector: What key to join on in inner sequence elements.
· Func<TOuter, IEnumerable<TInner>, TResult> resultSelector: What the output groups will look like.
Notice the final parameter here is different from Join’s (Func<TOuter, TInner, TResult> resultSelector). Rather than a single TInner, GroupJoin has IEnumerable<TInner>. This IEnumerable<TInner> contains all the elements from the inner sequence that match the outer sequence key; it is essentially all the inner sequence elements that belong in the group.
The following code shows GroupJoin in use. Notice the result selector is creating an anonymous type containing the group “key” (i.e. the recipe name), and a list of the reviews that belong to that recipe.
Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes .GroupJoin( reviews, (Recipe outerKey) => outerKey.Id, (Review innerKey) => innerKey.RecipeId, (Recipe recipe, IEnumerable<Review> rev => new { RecipeName = recipe.Name, Reviews = revs } ); foreach (var item in query) { Console.WriteLine("Reviews for {0}", item.RecipeName); foreach (var review in item.Reviews) { Console.WriteLine(" - {0}", review.ReviewText); } } |
This produces the following output:
Reviews for Mashed Potato
- Tasty!
- Not nice :(
- Pretty good
Reviews for Crispy Duck
- Too hard
- Loved it
Reviews for Sachertorte
Notice the “Sachertorte” group has been included even though it has no related reviews.
Query expression usage
The join keyword is used when using the query expression style; see Chapter 2, “Fluent and Query Expression Styles,” for usage of the join keyword. The following code shows a group join using the query expression style.
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId select new // anonymous type { RecipeName = recipe.Name, Reviews = reviewGroup // collection of related reviews }; |
The Zip query operator joins two sequences together, but unlike Join and GroupJoin, does not perform joining based on keys, and the concepts of inner and outer sequences do not apply. Instead, Zip simply interleaves each element from the two input sequences together, one after the other, much like joining two sides of a zipper on an item of clothing.
When using Zip, the resulting shape of the output elements is specified. In the following example, the result elements are of type Ingredient.
string[] names = {"Flour", "Butter", "Sugar"}; int[] calories = {100, 400, 500}; IEnumerable<Ingredient> ingredients = names.Zip(calories, (name, calorie) => new Ingredient { Name = name, Calories = calorie }); foreach (var item in ingredients) { Console.WriteLine("{0} has {1} calories", item.Name, item.Calories); } |
This produces the following output:
Flour has 100 calories
Butter has 400 calories
Sugar has 500 calories
If there is a greater number of elements in one of the two input sequences, these extra elements are ignored and do not appear in the output.
Query expression usage
There is no keyword for Zip when using the query expression style; however, mixed style can be used.