In some of the previous chapters, we illustrated how to use LINQ to perform queries on different types of data. Chapter 2 showed how to query collections and arrays, Chapter 6 to query XML trees, and Chapter 9 to query databases. This chapter shows you how to build on those simple examples to exploit the full flexibility of LINQ.
One of the best features of LINQ is that you can perform the same kinds of queries whatever the data source is. Each of the recipes in this chapter uses an array or a collection as the data source, but the same techniques can be applied equally to XML or databases. The recipes in this chapter are all self-contained and illustrate different LINQ features—but part of the allure of LINQ is that you will be able to combine these techniques to create complex and powerful queries. The recipes in this chapter describe how to perform the following tasks:
Filter elements in a data source (recipes 16-1, 16-2, and 16-3)
Create anonymous types in results (recipe 16-4)
Work with multiple data sources in queries (recipes 16-6, 16-7, ad 16-8)
Grouping, sorting, comparing, and aggregating data (recipes 16-9 through to 16-12)
Sharing interim results across query clauses (recipe 16-13)
Extending LINQ with custom extension methods (recipe 16-14)
Converting LINQ results into other types (recipe 16-15)
The most basic LINQ query selects all of the items contained in a data source. The most powerful aspect of LINQ is that you can apply the same query approach to any data source and get consistent, predictable results. Microsoft has embedded LINQ support throughout the .NET Framework so that you can use arrays, collections, XML documents and databases in the same way.
To select all of the items in a data source is a simple two-step process
Start a new LINQ query using the from
keyword, providing an element variable name that you will use to refer to elements that LINQ finds (for example, frome indatasource
).
Indicate what will be added to the result set from each matching element using the select
keyword.
For the basic "select all" query used in this recipe, we simply define the element variable name in the first step and use it as the basis for the second step, as follows:
IEnumerable<myType> myEnum = from e in datasource select e;
The type that you use for the datasource
reference must implement the System.Collections.Generic.IEnumerable
<>
interface. If you are using an array or a generic collection, then you can simply use the references to your instance as the data source because arrays and all standard generic collections implement IEnumerable
<>
, as follows:
IEnumerable<myType> myEnum = from e in myarray select e; IEnumerable<myType> myEnum = from e in mycollection select e;
If you are using an XML tree, you can get an IEnumerable
from the root XElement
by calling the Elements
method; and for a DataTable
, you can get an IEnumerable
by calling the AsEnumerable
method, as follows:
IEnumerable<XElement> myEnum = from e in root.Elements() select e; IEnumerable<DataRow> myEnum = from e in table.AsEnumerable() select e;
Notice that the generic type of the result is dependent on the data source. For an array or collection, the result will be an IEnumerable
of the data contained in the array—string
for string[]
and IList<string>
, for example. LINQ queries of DataTable
s return an IEnumerable<DataRow>
, and queries of an XML tree return IEnumerable<XElement>
.
If you want to select a value contained within a data source element (for example, if myType
had a property called Name
that returned a string
), then you simply specify the value you want after the select
keyword—for example:
IEnumerable<string> myEnum = from e in datasource select e.Name;
Notice that the generic type of the result has changed—we are querying a data source that contains myType
instances, but selecting a string
property—therefore, the result is an IEnumerable<string>
.
IEnumerable<>
can be used with a foreach
loop to enumerate the results of a query, but because LINQ queries return instances of IEnumerable<>
and LINQ data sources must implement IEnumerable<>
, you can also use the result of one query as the data source for another.
The following example performs a basic LINQ query on a string
array, a collection, an XML tree, and a DataTable
, and prints out the results in each case:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.Linq; using System.Data; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_01 { static void Main(string[] args) { Console.WriteLine("Using an array source"); // Create the source. string[] array = createArray(); // Perform the query. IEnumerable<string> arrayEnum = from e in array select e; // Write out the elements. foreach (string str in arrayEnum) { Console.WriteLine("Element {0}", str); } Console.WriteLine(" Using a collection source"); // Create the source. ICollection<string> collection = createCollection(); // Perform the query. IEnumerable<string> collEnum = from e in collection select e; // Write out the elements. foreach (string str in collEnum) { Console.WriteLine("Element {0}", str); } Console.WriteLine(" Using an xml source"); // Create the source. XElement xmltree = createXML();
// Perform the query. IEnumerable<XElement> xmlEnum = from e in xmltree.Elements() select e; // Write out the elements. foreach (string str in xmlEnum) { Console.WriteLine("Element {0}", str); } Console.WriteLine(" Using a data table source"); // Create the source. DataTable table = createDataTable(); // Perform the query. IEnumerable<string> dtEnum = from e in table.AsEnumerable() select e.Field<string>(0); // Write out the elements. foreach (string str in dtEnum) { Console.WriteLine("Element {0}", str); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static string[] createArray() { return new string[] { "apple", "orange", "grape", "fig", "plum", "banana", "cherry" }; } static IList<string> createCollection() { return new List<string>() { "apple", "orange", "grape", "fig", "plum", "banana", "cherry" }; } static XElement createXML() { return new XElement("fruit", new XElement("name", "apple"), new XElement("name", "orange"), new XElement("name", "grape"), new XElement("name", "fig"), new XElement("name", "plum"), new XElement("name", "banana"), new XElement("name", "cherry") ); }
static DataTable createDataTable() { DataTable table = new DataTable(); table.Columns.Add("name", typeof(string)); string[] fruit = { "apple", "orange", "grape", "fig", "plum", "banana", "cherry" }; foreach (string name in fruit) { table.Rows.Add(name); } return table; } } }
Running the example produces the following result:
Using an array source Element apple Element orange Element grape Element fig Element plum Element banana Element cherry Using a collection source Element apple Element orange Element grape Element fig Element plum Element banana
Element cherry Using an xml source Element apple Element orange Element grape Element fig Element plum Element banana Element cherry Using a data table source Element apple Element orange Element grape Element fig Element plum Element banana Element cherry Main method complete. Press Enter
Using the where
keyword in conjunction with the basic LINQ query covered in recipe 16-1 allows you to specify criteria that will be used to filter the contents of a data source. You supply an expression that will be evaluated for each element in the data source— an element will be included in the results if your expression returns true
and excluded if your expression returns false
. You can use the element variable declared with the from
keyword to refer to the current element.
For example, the following fragment uses the element variable to filter only string elements whose first character is t
:
string[] array = { "one", "two", "three", "four" }; IEnumerable<string> result = from e in array where e[0] == 't' select e;
You can make your filter expressions as complex as required and also call methods that return a bool
. A LINQ query can have multiple filters, such that the two LINQ queries in the following fragment are equivalent:
string[] array = { "one", "two", "three", "four" }; IEnumerable<string> result1 = from e in array where e[0] == 't' where e[1] == 'w' select e; IEnumerable<string> result2 = from e in array where e[0] == 't' && e[1] == 'w' select e;
The following example creates a collection of a type Fruit
and then filters the data using the LINQ where
operator using a string comparison and an arithmetic operator:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Xml.Linq;
namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_02 { static void Main(string[] args) { // Create the data. IList<Fruit> datasource = createData(); // Filter based on a single characteristic. IEnumerable<string> result1 = from e in datasource where e.Color == "green" select e.Name; Console.WriteLine("Filter for green fruit"); foreach (string str in result1) { Console.WriteLine("Fruit {0}", str); } // Filter based using > operator. IEnumerable<Fruit> result2 = from e in datasource where e.ShelfLife > 5 select e; Console.WriteLine(" Filter for life > 5 days"); foreach (Fruit fruit in result2) { Console.WriteLine("Fruit {0}", fruit.Name); } // Filter using two characteristics. IEnumerable<string> result3 = from e in datasource where e.Color == "green" && e.ShelfLife > 5 select e.Name; Console.WriteLine(" Filter for green fruit and life > 5 days"); foreach (string str in result3) { Console.WriteLine("Fruit {0}", str); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<Fruit> createData() { return new List<Fruit>() { new Fruit("apple", "green", 7), new Fruit("orange", "orange", 10), new Fruit("grape", "green", 4), new Fruit("fig", "brown", 12),
new Fruit("plum", "red", 2), new Fruit("banana", "yellow", 10), new Fruit("cherry", "red", 7) }; } } class Fruit { public Fruit(string namearg, string colorarg, int lifearg) { Name = namearg; Color = colorarg; ShelfLife = lifearg; } public string Name { get; set;} public string Color { get; set;} public int ShelfLife { get; set;} } }
C# has keywords for many LINQ features, but they are mappings to extension methods in the System.Linq
namespace—the keywords exist to simplify your code. See recipe 13-15 for a recipe to create and use an extension method. Keywords do not exist for all of the LINQ functions—some features are only available using extension methods directly. In order to filter a data source for all objects of a given type, you call the OfType<>
method, specifying the type that you are looking for, as the following code fragment shows:
IEnumerable <string> stringData = mixedData.OfType<string>();
The fragment filters the data source for all string instances and will omit any other type from the results. Notice that the result of calling OfType<>
is an IEnumeration<>
, which can be used as the data source for a further LINQ query, as shown by the following fragment, which filters a data source for all string instances and then filters the results for strings with the first character of c
:
IEnumerable<string> stringData = from e in mixedData.OfType<string>() where e[0] == 'c' select e;
You must import the System.Linq
namespace before you can use the LINQ extension methods with the using System.Linq
statement.
The following example creates a collection of mixed.
types and then filters the elements using the OfType
<>
extension method:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_03 { static void Main(string[] args) { IList<object> mixedData = createData(); IEnumerable <string> stringData = mixedData.OfType<string>(); foreach (string str in stringData) { Console.WriteLine(str); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<object> createData() { return new List<object>() { "this is a string", 23, 9.2, "this is another string" }; } } }
The Skip
<>
extension method omits the specified number of elements, starting at the beginning of the set of elements in the data source, and includes the remaining elements. The Take
<>
extension method does the opposite—it includes the specified number of elements and omits the rest. As with all of the LINQ extension methods, you must supply a generic type annotation when calling the method—this determines the type of IEnumeration<>
that will be returned. The Range<>
extension method takes a start index and a count as method parameters and returns a subset of the elements in the data source.
Skip<>, Take<>
, and Range<>
all return IEnumeration<>
, so the results from these methods can be used either to enumerate the results or as the data source for another LINQ query.
The following example creates a string
array and uses it as the data source for Take
and Skip
filters:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_04 { static void Main(string[] args) { string[] array = { "one", "two", "three", "four", "five" }; IEnumerable<string> skipresult = from e in array.Skip<string>(2) select e; foreach (string str in skipresult) { Console.WriteLine("Result from skip filter: {0}", str); }
IEnumerable<string> takeresult = from e in array.Take<string>(2) select e; foreach (string str in takeresult) { Console.WriteLine("Result from take filter: {0}", str); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the example program gives the following results:
Result from skip filter: three Result from skip filter: four Result from skip filter: five Result from take filter: one Result from take filter: two Main method complete. Press Enter
If you want to create a LINQ result that contains the values from more than one member of a data element, you can use the new
keyword after the select
keyword to create an anonymous type. An anonymous type doesn't have a name (hence "anonymous") and is made up of just the values that you specify.
You reference the result from the query using the special var
type, as shown in the example code.
The following example creates a collection of the Fruit
type and then performs a LINQ query that returns an anonymous type containing the Name
and Color
properties from each Fruit
element:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_05 { static void Main(string[] args) { IList<Fruit> sourcedata = createData(); var result = from e in sourcedata select new { e.Name, e.Color }; foreach (var element in result) { Console.WriteLine("Result: {0} {1}", element.Name, element.Color); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<Fruit> createData() { return new List<Fruit>() { new Fruit("apple", "green", 7), new Fruit("orange", "orange", 10), new Fruit("grape", "green", 4), new Fruit("fig", "brown", 12), new Fruit("plum", "red", 2),
new Fruit("banana", "yellow", 10), new Fruit("cherry", "red", 7) }; } } class Fruit { public Fruit(string namearg, string colorarg, int lifearg) { Name = namearg; Color = colorarg; ShelfLife = lifearg; } public string Name { get; set; } public string Color { get; set; } public int ShelfLife { get; set; } } }
You need to create an anonymous type that contains values from multiple data sources with common keys.
If you have two data sources that share a common key, you can combine them in a LINQ query using the join...in...on...equals
...
keywords. The following fragment demonstrates how to do this:
from e in firstDataSource join f in secondDataSource on e.CommonKey equals f.CommonKey
LINQ will arrange the data so that your filter and select
statements are called once per common key. You can refer to the individual elements using the variable names you have defined—in the fragment, we have used e
and f
. You can join as many data sources as you wish in a LINQ query, as long as they share a common key.
The following example creates two data sources that share a common key, and uses the join
keyword to combine them in a LINQ query in order to create an anonymous result type that contains elements from both data sources:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_6 { static void Main(string[] args) { // Create the data sources. IList<FruitColor> colorsource = createColorData(); IList<FruitShelfLife> shelflifesource = createShelfLifeData(); // Perform the LINQ query with a join. var result = from e in colorsource join f in shelflifesource on e.Name equals f.Name where e.Color == "green" select new { e.Name, e.Color, f.Life }; // Write out the results. foreach (var element in result) { Console.WriteLine("Name: {0}, Color: {1}, Shelf Life: {2} days", element.Name, element.Color, element.Life); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<FruitColor> createColorData() { return new List<FruitColor>() { new FruitColor("apple", "green"), new FruitColor("orange", "orange"), new FruitColor("grape", "green"),
new FruitColor("fig", "brown"), new FruitColor("plum", "red"), new FruitColor("banana", "yellow"), new FruitColor("cherry", "red") }; } static IList<FruitShelfLife> createShelfLifeData() { return new List<FruitShelfLife>() { new FruitShelfLife("apple", 7), new FruitShelfLife("orange", 10), new FruitShelfLife("grape", 4), new FruitShelfLife("fig", 12), new FruitShelfLife("plum", 2), new FruitShelfLife("banana", 10), new FruitShelfLife("cherry", 7) }; } } class FruitColor { public FruitColor(string namearg, string colorarg) { Name = namearg; Color = colorarg; } public string Name { get; set; } public string Color { get; set; } } class FruitShelfLife { public FruitShelfLife(string namearg, int lifearg) { Name = namearg; Life = lifearg; } public string Name { get; set; } public int Life{ get; set; } } }
Running the example gives the following results:
Name: apple Color green Shelf Life: 7 days Name: grape Color green Shelf Life: 4 days Main method complete. Press Enter
You can enumerate through the permutations of multiple data sources by using more than one from
keyword in your LINQ query. The query will be applied to every permutation of every element in each data source. The following fragment illustrates a query that uses from
twice:
string[] datasource1 = { "apple", "orange",}; int[] datasource2 = { 21, 42 }; var result = from e in datasource1 from f in datasource2 select new { e, f };
The select
part of the query (and any filters that we might have applied) will be called for every combination of element from the two data sources—apple
and 21, apple
and 42, orange
and 21
, and orange
and 42
.
The following example creates two arrays and uses them as data sources for a LINQ query with multiple from keywords. The result is an anonymous type containing the elements from both sources, and each element in the result is printed to the console.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_7 { static void Main(string[] args) { // Create the data sources. string[] datasource1 = { "apple", "orange", "cherry", "pear" }; int[] datasource2 = { 21, 42, 37 }; // Perform the LINQ query. var result = from e in datasource1 from f in datasource2 select new { e, f }; // Print the results. foreach (var element in result) { Console.WriteLine("{0}, {1}", element.e, element.f); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the program produces the following results:
apple, 21 apple, 42 apple, 37 orange, 21 orange, 42 orange, 37 cherry, 21 cherry, 42 cherry, 37 pear, 21 pear, 42 pear, 37 Main method complete. Press Enter
Use the Concat<>
extension method to combine multiple sources into a sequence that LINQ will process as a single data source.
The Concat<>
extension method returns an IEnumeration<>
containing the element in the data source on which you call the method and the elements in the data source you pass as a method parameter. The type annotation you pass to the Concat<>
method must match the element types in the data sources.
The following example concatenates two arrays of strings to form a single data source for a LINQ query:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_08 { static void Main(string[] args) { // Create the data sources. string[] datasource1 = { "apple", "orange", "cherry", "pear" }; string[] datasource2 = { "banana", "kiwi", "fig" }; // Perform the LINQ query. IEnumerable<string> result = from e in datasource1.Concat<string>(datasource2) select e; // Print the results. foreach (string element in result) { Console.WriteLine(element); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the example produces the following result:
apple orange cherry pear banana kiwi fig Main method complete. Press Enter
You need to order the result of a LINQ query so that elements that share a common attribute are grouped together.
The group
...by...
keywords allow you to create a result where elements that share a member value are grouped together. Using group
...by...
in a query results in a fragment such as this:
IEnumerable<IGrouping<T1, T2>> result = from e in datasource group e.FirstMember by e.SecondMember;
The result from this query in an instance of System.Linq.IGrouping<T1, T2>
, where T1
is the type of e.SecondMember
and T2
is the type of e.FirstMember
. All of the elements in the data source that have the same value of e.SecondMember
will appear in the same IGrouping<>
, and there will be an instance of IGrouping<>
contained in the IEnumeration<>
for each distinct value of e.SecondMember
that LINQ finds in the data source. The easiest way to understand these keywords is to review and run the example program that follows.
You can get the value that the elements contained in an IGrouping<>
share by calling the Key
property. IGrouping<>
extends IEnumerable<>
, so you can enumerate the values of a group using a foreach
loop or use an IGrouping<>
as the data source for a LINQ query.
You can access each individual group as it is created by LINQ using the order...by...into...
keywords. The addition of into
allows you to define a variable that will contain the IGrouping<>
instance—see the example program for this recipe to see an example of using into
to create an anonymous type.
The following example uses a collection of the type Fruit
as the data source for two LINQ queries. The first uses a standard group
...by...
format to create an IEnumerable<IGrouping<string, Fruit>>
result, which is then enumerated by group and the elements in each group. The second query uses group
...by...into
...
in order to create an anonymous type containing the Key
value of the group and the set of matching Fruit
instances, which are then printed out.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_09 { static void Main(string[] args) { // Create the data source. IList<Fruit> datasource = createData(); Console.WriteLine("Perfoming group...by... query"); // Perform a query with a basic grouping. IEnumerable<IGrouping<string, Fruit>> result = from e in datasource group e by e.Color; foreach (IGrouping<string, Fruit> group in result) { Console.WriteLine(" Start of group for {0}", group.Key); foreach (Fruit fruit in group) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", fruit.Name, fruit.Color, fruit.ShelfLife); } }
Console.WriteLine(" Perfoming group...by...into query"); // Use the group...by...into... keywords. var result2 = from e in datasource group e by e.Color into g select new { Color = g.Key, Fruits = g }; foreach (var element in result2) { Console.WriteLine(" Element for color {0}", element.Color); foreach (var fruit in element.Fruits) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", fruit.Name, fruit.Color, fruit.ShelfLife); } } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<Fruit> createData() { return new List<Fruit>() { new Fruit("apple", "green", 7), new Fruit("orange", "orange", 10), new Fruit("grape", "green", 4), new Fruit("fig", "brown", 12), new Fruit("plum", "red", 2), new Fruit("banana", "yellow", 10), new Fruit("cherry", "red", 7) }; } } class Fruit { public Fruit(string namearg, string colorarg, int lifearg) { Name = namearg; Color = colorarg; ShelfLife = lifearg; }
public string Name { get; set; } public string Color { get; set; } public int ShelfLife { get; set; } } }
Running the example program produces the following results:
Perfoming order...by... query Start of group for green Name: apple Color: green Shelf Life: 7 days. Name: grape Color: green Shelf Life: 4 days. Start of group for orange Name: orange Color: orange Shelf Life: 10 days. Start of group for brown Name: fig Color: brown Shelf Life: 12 days. Start of group for red Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days.
Start of group for yellow Name: banana Color: yellow Shelf Life: 10 days. Perfoming order...by...into query Element for color green Name: apple Color: green Shelf Life: 7 days. Name: grape Color: green Shelf Life: 4 days. Element for color orange Name: orange Color: orange Shelf Life: 10 days. Element for color brown Name: fig Color: brown Shelf Life: 12 days. Element for color red Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days. Element for color yellow Name: banana Color: yellow Shelf Life: 10 days. Main method complete. Press Enter
The orderby
keyword sorts the result elements of a LINQ query by the member you specify. You can sort on several members by using the orderby
keyword more than once—see the example code for this recipe for an illustration. By default, LINQ will sort the elements in ascending order (the smallest value will come first in the results)—you can use the descending
keyword after the member you want to use for sorting to get the reverse effect.
The following example creates a collection containing the Fruit
type and uses it as the basis for a LINQ query that orders the results by the Color
property in descending order and then the Name
property in ascending order.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_10 { static void Main(string[] args) { // Create the data source. IList<Fruit> datasource = createData(); IEnumerable<Fruit> result = from e in datasource orderby e.Name orderby e.Color descending select e; foreach (Fruit fruit in result) {
Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", fruit.Name, fruit.Color, fruit.ShelfLife); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<Fruit> createData() { return new List<Fruit>() { new Fruit("apple", "red", 7), new Fruit("apple", "green", 7), new Fruit("orange", "orange", 10), new Fruit("grape", "green", 4), new Fruit("fig", "brown", 12), new Fruit("plum", "red", 2), new Fruit("banana", "yellow", 10), new Fruit("cherry", "red", 7) }; } } class Fruit { public Fruit(string namearg, string colorarg, int lifearg) { Name = namearg; Color = colorarg; ShelfLife = lifearg; } public string Name { get; set; } public string Color { get; set; } public int ShelfLife { get; set; } } }
Running the program gives the following results:
Name: apple Color: red Shelf Life: 7 days. Name: apple Color: green Shelf Life: 7 days. Name: banana Color: yellow Shelf Life: 10 days. Name: cherry Color: red Shelf Life: 7 days. Name: fig Color: brown Shelf Life: 12 days.
Name: grape Color: green Shelf Life: 4 days. Name: orange Color: orange Shelf Life: 10 days. Name: plum Color: red Shelf Life: 2 days. Main method complete. Press Enter
The SequenceEquals<>
extension method compares two data sources and returns true
if both data sources contain the same number of elements and the individual elements in each position in each data source are the same. You can specify your own code to assess element equality by implementing the System.Collections.Generic.IEqualityComparer<
>
interface and supplying an instance of the implementation as an argument to SequenceEquals<>
.
The following example creates four data sources. The first contains a list of names of fruit. The second contains the same names in the same order. The third contains the same names in a different order, and the last contains different names, but with the same first letters as the names in the first list. Comparisons are then performed using the default IEqualityComparer
and a custom IEqualityComparer
that treats strings with the same first character as being equal.
using System; using System.Collections.Generic; using System.Linq; using System.Text;
namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_11 { static void Main(string[] args) { // Create the first data source. string[] ds1 = { "apple", "cherry", "pear" }; // Create a data source with the same elements // in the same order. string[] ds2 = { "apple", "cherry", "pear" }; // Create a data source with the // same elements in a different order. string[] ds3 = { "pear", "cherry", "apple" }; // Create a data source with different elements. string[] ds4 = { "apricot", "cranberry", "plum" }; // Perform the comparisons. Console.WriteLine("Using standard comparer"); Console.WriteLine("DS1 == DS2? {0}", ds1.SequenceEqual(ds2)); Console.WriteLine("DS1 == DS3? {0}", ds1.SequenceEqual(ds3)); Console.WriteLine("DS1 == DS4? {0}", ds1.SequenceEqual(ds4)); // Create the custom comparer. MyComparer comparer = new MyComparer(); Console.WriteLine(" Using custom comparer"); Console.WriteLine("DS1 == DS2? {0}", ds1.SequenceEqual(ds2, comparer)); Console.WriteLine("DS1 == DS3? {0}", ds1.SequenceEqual(ds3, comparer)); Console.WriteLine("DS1 == DS4? {0}", ds1.SequenceEqual(ds4, comparer)); // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } class MyComparer : IEqualityComparer<string> { public bool Equals(string first, string second) { return first[0] == second[0]; } public int GetHashCode(string str) { return str[0].GetHashCode(); } } }
Running the program gives the following results:
Using standard comparer DS1 == DS2? True DS1 == DS3? False DS1 == DS4? False Using custom comparer DS1 == DS2? True DS1 == DS3? False DS1 == DS4? True Main method complete. Press Enter
Use the Average<>, Count<>, Max<>, Min<>
, or Sum<>
extension methods for standard aggregations, or the Aggregate<>
extension method to perform a custom aggregation.
The standard aggregation extension methods process the elements in a data source to perform useful calculations. Average<>
calculates the mean value, Count<>
returns the number of elements in the data source, Min<>
and Max<>
return the smallest and largest elements, and Sum<>
totals the elements.
You can perform custom aggregation operations using the Aggregate<>
method. The example code demonstrates two custom aggregation operations. The expression receives two arguments—the first is the aggregate value so far and the second is the current element to process. The parameters and return value are of the same type as the data source type—that is, if you are aggregating an IEnumeration<string>
, you will receive two string
s as arguments and must return a string
as your aggregate result.
The following example creates a data source of integers and calls each of the standard aggregation methods. The same data source is used to demonstrate a custom aggregation method that totals the individual elements (equivalent to the Sum<>
method). Finally, a string
array is used as a data source for a custom aggregation that concatenates the individual elements.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_12 { static void Main(string[] args) { // Define a numeric data source. int[] ds1 = { 1, 23, 37, 49, 143 }; // Use the standard aggregation methods. Console.WriteLine("Standard aggregation methods"); Console.WriteLine("Average: {0}", ds1.Average()); Console.WriteLine("Count: {0}", ds1.Count()); Console.WriteLine("Max: {0}", ds1.Max()); Console.WriteLine("Min: {0}", ds1.Min()); Console.WriteLine("Sum: {0}", ds1.Sum()); // Perform our own sum aggregation. Console.WriteLine(" Custom aggregation"); Console.WriteLine(ds1.Aggregate((total, elem) => total += elem)); // Define a string data source. string[] ds2 = { "apple", "pear", "cherry" }; // Perform a concat aggregation. Console.WriteLine(" String concatenation aggregation"); Console.WriteLine(ds2.Aggregate((len, elem) => len += elem));
// Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } }
The program gives the following results:
Standard aggregation methods Average: 50.6 Count: 5 Max: 143 Min: 1 Sum: 253 Custom aggregation 253 String concatenation aggregation applepearcherry Main method complete. Press Enter
If you need to perform the same operation in different parts of your query, you can store the result of an expression and use it several times. The example for this recipe demonstrates using the Sum
aggregate method and using the result in both the where
and select
clauses of the query. Without the use of the let
keyword, we would have to perform the aggregation in each clause. You can use let
multiple times in a query.
The following example demonstrates how to use the let
keyword to obtain the sum of the elements in a data source consisting of integers and use the result in the where
and select
sections of the same LINQ query.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_13 { static void Main(string[] args) { // Define a numeric data source. int[] ds1 = { 1, 23, 37, 49, 143 }; // Perform a query that shares a calculated value. IEnumerable<double> result1 = from e in ds1 let avg = ds1.Average() where (e < avg) select (e + avg); Console.WriteLine("Query using shared value"); foreach (double element in result1) { Console.WriteLine("Result element {0}", element); }
// Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the program gives the following results:
Query using shared value Result element 51.6 Result element 73.6 Result element 87.6 Result element 99.6 Main method complete. Press Enter
Recipe 13-15 demonstrates how to create and use an extension method. The process for a LINQ extension method is the same, except that you specify the type to operate on as IEnumerable<>
. All LINQ data sources implement IEnumerable<>
or have a member that returns IEnumerable<>
, so once you have defined your extension method, you will be able to apply it to any data source that contains elements of the type you have specified.
The following example demonstrates creating a customer LINQ extension method that removes the first and last element from string data sources:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { static class LINQExtensions { public static IEnumerable<string> RemoveFirstAndLast( this IEnumerable<string> source) { return source.Skip(1).Take(source.Count() - 2); } } class Recipe16_14 { static void Main(string[] args) { // Create the data sources. string[] ds1 = {"apple", "banana", "pear", "fig"}; IList<string> ds2 = new List<string> { "apple", "banana", "pear", "fig" }; Console.WriteLine("Extension method used on string[]"); IEnumerable<string> result1 = ds1.RemoveFirstAndLast(); foreach (string element in result1) { Console.WriteLine("Result: {0}", element); } Console.WriteLine(" Extension method used on IList<string>"); IEnumerable<string> result2 = ds1.RemoveFirstAndLast(); foreach (string element in result2) { Console.WriteLine("Result: {0}", element); } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the sample program gives the same result for the differing data sources:
Extension method used on string[] Result: banana Result: pear Extension method used on IList<string> Result: banana Result: pear Main method complete. Press Enter
It is not always convenient to have the results of a query as an IEnumerable
. LINQ provides a series of extension methods that you can use to convert a query result into different types.
The following example creates a data source containing instances of the type Fruit
, performs a LINQ query to select those with a short shelf life, and then converts the result to an array, a Dictionary
, a List
, and a Lookup
, printing out the contents of each:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter16 { class Recipe16_15 { static void Main(string[] args) { // Create the data sources. IEnumerable<Fruit> datasource = createData(); // Perform a query. IEnumerable<Fruit> result = from e in datasource where e.ShelfLife <= 7 select e; // Enumerate the result elements. Console.WriteLine("Results from enumeration"); foreach (Fruit fruit in result) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", fruit.Name, fruit.Color, fruit.ShelfLife); } // Convert the IEnumerable to an array. Fruit[] array = result.ToArray<Fruit>(); // print out the contents of the array Console.WriteLine(" Results from array"); for (int i = 0; i < array.Length; i++) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", array[i].Name, array[i].Color, array[i].ShelfLife); } // Convert the IEnumerable to a dictionary indexed by name. Dictionary<string, Fruit> dictionary = result.ToDictionary(e => e.Name); // print out the contents of the dictionary Console.WriteLine(" Results from dictionary"); foreach (KeyValuePair<string, Fruit> kvp in dictionary) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", kvp.Key, kvp.Value.Color, kvp.Value.ShelfLife); }
// Convert the IEnumerable to a list. IList<Fruit> list = result.ToList<Fruit>(); // print out the contents of the list Console.WriteLine(" Results from list"); foreach (Fruit fruit in list) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", fruit.Name, fruit.Color, fruit.ShelfLife); } // Convert the IEnumerable to a lookup, indexed by color. ILookup<string, Fruit> lookup = result.ToLookup(e => e.Color); // Print out the contents of the list. Console.WriteLine(" Results from lookup"); IEnumerator<IGrouping<string, Fruit>> groups = lookup.GetEnumerator(); while (groups.MoveNext()) { IGrouping<string, Fruit> group = groups.Current; Console.WriteLine("Group for {0}", group.Key); foreach (Fruit fruit in group) { Console.WriteLine("Name: {0} Color: {1} Shelf Life: {2} days.", fruit.Name, fruit.Color, fruit.ShelfLife); } } // Wait to continue. Console.WriteLine(" Main method complete. Press Enter"); Console.ReadLine(); } static IList<Fruit> createData() { return new List<Fruit>() { new Fruit("apple", "red", 7), new Fruit("orange", "orange", 10), new Fruit("grape", "green", 4), new Fruit("fig", "brown", 12), new Fruit("plum", "red", 2), new Fruit("banana", "yellow", 10), new Fruit("cherry", "red", 7) }; } } class Fruit {
public Fruit(string namearg, string colorarg, int lifearg) { Name = namearg; Color = colorarg; ShelfLife = lifearg; } public string Name { get; set; } public string Color { get; set; } public int ShelfLife { get; set; } } }
Running the program gives the following results:
Results from enumeration Name: apple Color: red Shelf Life: 7 days. Name: grape Color: green Shelf Life: 4 days. Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days. Results from array Name: apple Color: red Shelf Life: 7 days. Name: grape Color: green Shelf Life: 4 days. Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days. Results from dictionary Name: apple Color: red Shelf Life: 7 days. Name: grape Color: green Shelf Life: 4 days. Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days.
Results from list Name: apple Color: red Shelf Life: 7 days. Name: grape Color: green Shelf Life: 4 days. Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days. Results from lookup Group for red Name: apple Color: red Shelf Life: 7 days. Name: plum Color: red Shelf Life: 2 days. Name: cherry Color: red Shelf Life: 7 days. Group for green Name: grape Color: green Shelf Life: 4 days. Main method complete. Press Enter