Pro C#10 CHAPTER 13 LINQ to Objects

CHAPTER 13

LINQ to Objects

Regardless of the type of application you are creating using the .NET platform, your program will certainly need to access some form of data as it executes. To be sure, data can be found in numerous locations including XML files, relational databases, in-memory collections, and primitive arrays. Historically speaking, based on the location of said data, programmers needed to use different and unrelated APIs. The Language Integrated Query (LINQ) technology set, introduced initially in .NET 3.5, provides a concise, symmetrical, and strongly typed manner to access a wide variety of data stores. In this chapter, you will begin your investigation of LINQ by focusing on LINQ to Objects.
Before you dive into LINQ to Objects proper, the first part of this chapter quickly reviews the key C# programming constructs that enable LINQ. As you work through this chapter, you will find that implicitly typed local variables, object initialization syntax, lambda expressions, extension methods, and anonymous types will be quite useful (if not occasionally mandatory).
After this supporting infrastructure is reviewed, the remainder of the chapter will introduce you to the LINQ programming model and its role in the .NET platform. Here, you will come to learn the role of query operators and query expressions, which allow you to define statements that will interrogate a data source to yield the requested result set. Along the way, you will build numerous LINQ examples that interact with data contained within arrays as well as various collection types (both generic and nongeneric) and understand the assemblies, namespaces, and types that represent the LINQ to Objects API.

■ Note The information in this chapter is the foundation for future sections and chapters of this book, including Parallel LINQ (Chapter 15) and Entity Framework Core (Chapters 21 through 23).

LINQ-Specific Programming Constructs
From a high level, LINQ can be understood as a strongly typed query language, embedded directly into the grammar of C#. Using LINQ, you can build any number of expressions that have a look and feel like that of a database SQL query. However, a LINQ query can be applied to any number of data stores, including stores that have nothing to do with a literal relational database.

■ Note Although LINQ queries can look similar to SQL queries, the syntax is not identical. In fact, many LINQ queries seem to be the exact opposite format of a similar database query! If you attempt to map LINQ directly to SQL, you will surely become frustrated. To keep your sanity, I recommend you try your best to regard LINQ queries as unique statements, which just “happen to look” like SQL.

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_13

509

When LINQ was first introduced to the .NET platform in version 3.5, the C# and VB languages were each expanded with many new programming constructs used to support the LINQ technology set. Specifically, the C# language uses the following core LINQ-centric features:
•Implicitly typed local variables
•Object/collection initialization syntax
•Lambda expressions
•Extension methods
•Anonymous types
These features have already been explored in detail within various chapters of the text. However, to get the ball rolling, let’s quickly review each feature in turn, just to make sure we are all in the proper mindset.

■ Note Because the following sections are reviews of material covered elsewhere in the book, I have not included a C# code project for this content.

Implicit Typing of Local Variables
In Chapter 3, you learned about the var keyword of C#. This keyword allows you to define a local variable without explicitly specifying the underlying data type. The variable, however, is strongly typed, as the compiler will determine the correct data type based on the initial assignment. Recall this code example from Chapter 3:

static void DeclareImplicitVars()
{
// Implicitly typed local variables. var myInt = 0;
var myBool = true;
var myString = "Time, marches on…";

// Print out the underlying type.
Console.WriteLine("myInt is a: {0}", myInt.GetType().Name); Console.WriteLine("myBool is a: {0}",
myBool.GetType().Name); Console.WriteLine("myString is a: {0}",
myString.GetType().Name);
}

This language feature is helpful, and often mandatory, when using LINQ. As you will see during this chapter, many LINQ queries will return a sequence of data types, which are not known until compile time. Given that the underlying data type is not known until the application is compiled, you obviously can’t declare a variable explicitly!

Object and Collection Initialization Syntax
Chapter 5 explored the role of object initialization syntax, which allows you to create a class or structure variable and to set any number of its public properties in one fell swoop. The result is a compact (yet still easy on the eyes) syntax that can be used to get your objects ready for use. Also recall from Chapter 10, the C# language allows you to use a similar syntax to initialize collections of objects. Consider the following code snippet, which uses collection initialization syntax to fill a List of Rectangle objects, each of which maintains two Point objects to represent an (x,y) position:

List myListOfRects = new List
{
new Rectangle {TopLeft = new Point { X = 10, Y = 10 }, BottomRight = new Point { X = 200, Y = 200}},
new Rectangle {TopLeft = new Point { X = 2, Y = 2 }, BottomRight = new Point { X = 100, Y = 100}},
new Rectangle {TopLeft = new Point { X = 5, Y = 5 }, BottomRight = new Point { X = 90, Y = 75}}
};

While you are never required to use collection/object initialization syntax, doing so results in a more compact code base. Furthermore, this syntax, when combined with implicit typing of local variables, allows you to declare an anonymous type, which is useful when creating a LINQ projection. You’ll learn about LINQ projections later in this chapter.

Lambda Expressions
The C# lambda operator (=>) was fully explored in Chapter 12. Recall that this operator allows you to build a lambda expression, which can be used any time you invoke a method that requires a strongly typed delegate as an argument. Lambdas greatly simplify how you work with delegates in that they reduce the amount of code you must author by hand. Recall that a lambda expression can be broken down into the following usage:

( ArgumentsToProcess ) => { StatementsToProcessThem }

In Chapter 12, I walked you through how to interact with the FindAll() method of the generic List class using three different approaches. After working with the raw Predicate delegate and a C# anonymous method, you eventually arrived at the following (extremely concise) iteration with this lambda expression:

static void LambdaExpressionSyntax()
{
// Make a list of integers. List list = new List();
list.AddRange(new int[] { 20, 1, 4, 8, 9, 44 });

// C# lambda expression.
List evenNumbers = list.FindAll(i => (i % 2) == 0);

Console.WriteLine("Here are your even numbers:"); foreach (int evenNumber in evenNumbers)
{

Console.Write("{0}\t", evenNumber);
}
Console.WriteLine();
}

Lambdas will be useful when working with the underlying object model of LINQ. As you will soon find out, the C# LINQ query operators are simply a shorthand notation for calling true-blue methods on a class named System.Linq.Enumerable. These methods typically require delegates (the Func<> delegate in
particular) as parameters, which are used to process your data to yield the correct result set. Using lambdas, you can streamline your code and allow the compiler to infer the underlying delegate.

Extension Methods
C# extension methods allow you to tack on new functionality to existing classes without the need to subclass.
As well, extension methods allow you to add new functionality to sealed classes and structures, which could never be subclassed in the first place. Recall from Chapter 11, when you author an extension method, the first parameter is qualified with the this keyword and marks the type being extended. Also recall that extension methods must always be defined within a static class and must, therefore, also be declared using the static keyword. Here’s an example:

namespace MyExtensions; static class ObjectExtensions
{
// Define an extension method to System.Object.
public static void DisplayDefiningAssembly(this object obj)
{
Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()));
}
}

To use this extension, an application must import the namespace defining the extension (and possibly add a reference to the external assembly). At this point, simply import the defining namespace and
code away.

// Since everything extends System.Object, all classes and structures
// can use this extension. int myInt = 12345678;
myInt.DisplayDefiningAssembly();

System.Data.DataSet d = new System.Data.DataSet(); d.DisplayDefiningAssembly();

When you are working with LINQ, you will seldom, if ever, be required to manually build your own extension methods. However, as you create LINQ query expressions, you will be making use of numerous extension methods already defined by Microsoft. In fact, each C# LINQ query operator is a shorthand notation for making a manual call on an underlying extension method, typically defined by the System. Linq.Enumerable utility class.

Anonymous Types
The final C# language feature I’d like to quickly review is that of anonymous types, which were explored in Chapter 11. This feature can be used to quickly model the “shape” of data by allowing the compiler to generate a new class definition at compile time, based on a supplied set of name-value pairs. Recall that
this type will be composed using value-based semantics, and each virtual method of System.Object will be overridden accordingly. To define an anonymous type, declare an implicitly typed variable and specify the data’s shape using object initialization syntax.

// Make an anonymous type that is composed of another. var purchaseItem = new {
TimeBought = DateTime.Now, ItemBought =
new {Color = "Red", Make = "Saab", CurrentSpeed = 55}, Price = 34.000};

LINQ makes frequent use of anonymous types when you want to project new forms of data on the fly. For example, assume you have a collection of Person objects and want to use LINQ to obtain information on the age and Social Security number of each. Using a LINQ projection, you can allow the compiler to generate a new anonymous type that contains your information.

Understanding the Role of LINQ
That wraps up the quick review of the C# language features that allow LINQ to work its magic. However, why have LINQ in the first place? Well, as software developers, it is hard to deny that a significant amount of programming time is spent obtaining and manipulating data. When speaking of “data,” it is easy to
immediately envision information contained within relational databases. However, another popular location for data is within XML documents or simple text files.
Data can be found in numerous places beyond these two common homes for information. For instance, say you have an array or generic List type containing 300 integers and you want to obtain a subset that meets a given criterion (e.g., only the odd or even members in the container, only prime numbers, only nonrepeating numbers greater than 50). Or perhaps you are making use of the reflection APIs and need
to obtain only metadata descriptions for each class deriving from a parent class within an array of Types. Indeed, data is everywhere.
Prior to .NET 3.5, interacting with a flavor of data required programmers to use very diverse APIs.
Consider, for example, Table 13-1, which illustrates several common APIs used to access various types of data (I’m sure you can think of many other examples).

Table 13-1. Ways to Manipulate Various Types of Data

The Data You Want How to Obtain It
Relational data System.Data.dll, System.Data.SqlClient.dll, etc.
XML document data System.Xml.dll
Metadata tables The System.Reflection namespace
Collections of objects System.Array and the System.Collections/System.Collections.Generic
namespaces

Of course, nothing is wrong with these approaches to data manipulation. In fact, you can (and will) certainly make direct use of ADO.NET, the XML namespaces, reflection services, and the various collection types. However, the basic problem is that each of these APIs is an island unto itself, which offers little
in the way of integration. True, it is possible (for example) to save an ADO.NET DataSet as XML and then manipulate it via the System.Xml namespaces, but nonetheless, data manipulation remains rather asymmetrical.
The LINQ API is an attempt to provide a consistent, symmetrical way programmers can obtain and manipulate “data” in the broad sense of the term. Using LINQ, you can create directly within the C# programming language constructs called query expressions. These query expressions are based on numerous query operators that have been intentionally designed to look and feel similar (but not quite identical) to a SQL expression.
The twist, however, is that a query expression can be used to interact with numerous types of data— even data that has nothing to do with a relational database. Strictly speaking, “LINQ” is the term used to describe this overall approach to data access. However, based on where you are applying your LINQ queries, you will encounter various terms, such as the following:
•LINQ to Objects: This term refers to the act of applying LINQ queries to arrays and collections.
•LINQ to XML: This term refers to the act of using LINQ to manipulate and query XML documents.
•LINQ to Entities: This aspect of LINQ allows you to make use of LINQ queries within the ADO.NET Entity Framework (EF) Core API.
•Parallel LINQ (aka PLINQ): This allows for parallel processing of data returned from a LINQ query.
Today, LINQ is an integral part of the .NET base class libraries, managed languages, and Visual Studio itself.

LINQ Expressions Are Strongly Typed
It is also important to point out that a LINQ query expression (unlike a traditional SQL statement) is strongly typed. Therefore, the C# compiler will keep you honest and make sure that these expressions are syntactically well formed. Tools such as Visual Studio and Visual Studio Code can use metadata for useful features such as IntelliSense, autocompletion, and so forth.

The Core LINQ Assemblies
The LINQ assemblies are contained in the System.Linq namespace, which is provided by the .NET Framework as an implicit global using.

Applying LINQ Queries to Primitive Arrays
To begin examining LINQ to Objects, let’s build an application that will apply LINQ queries to various array objects. Create a Console Application project named LinqOverArray and define a static helper method within the Program.cs file named QueryOverStrings(). In this method, create a string array containing six or so items of your liking (here, I listed a batch of video games in my library). Make sure to have at least two entries that contain numerical values and a few that have embedded spaces.

static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
}

Now, update Program.cs to invoke QueryOverStrings().

Console.WriteLine(" Fun with LINQ to Objects \n"); QueryOverStrings();
Console.ReadLine();

When you have any array of data, it is common to extract a subset of items based on a given requirement. Maybe you want to obtain only the subitems that contain a number (e.g., System Shock 2, Uncharted 2, and Fallout 3), have some number of characters, or don’t contain embedded spaces (e.g., Morrowind or Daxter). While you could certainly perform such tasks using members of the System.Array type and a bit of elbow grease, LINQ query expressions can greatly simplify the process.
Going on the assumption that you want to obtain from the array only items that contain an embedded blank space and you want these items listed in alphabetical order, you could build the following LINQ query expression:

static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build a query expression to find the items in the array
// that have an embedded space. IEnumerable subset =
from g in currentVideoGames where g.Contains(" ") orderby g
select g;

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}

Notice that the query expression created here makes use of the from, in, where, orderby, and select
LINQ query operators. You will dig into the formalities of query expression syntax later in this chapter. However, even now you should be able to read this statement roughly as “Give me the items inside of currentVideoGames that contain a space, ordered alphabetically.”
Here, each item that matches the search criteria has been given the name g (as in “game”); however, any
valid C# variable name would do.

IEnumerable subset = from game in currentVideoGames where game.Contains(" ") orderby game
select game;

Notice that the returned sequence is held in a variable named subset, typed as a type that implements the generic version of IEnumerable, where T is of type System.String (after all, you are querying an array of strings). After you obtain the result set, you then simply print out each item using a standard foreach construct. If you run your application, you will find the following output:

Fun with LINQ to Objects Item: Fallout 3
Item: System Shock 2 Item: Uncharted 2

Once Again, Using Extension Methods
The LINQ syntax used earlier (and the rest of this chapter) is referred to as LINQ query expressions, which is a format that is like SQL but slightly different. There is another syntax that uses extension methods that will be the syntax used in most of the examples in this book.
Create a new method named QueryOverStringsWithExtensionMethods() and enter the following code:

static void QueryOverStringsWithExtensionMethods()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build a query expression to find the items in the array
// that have an embedded space.
IEnumerable subset =
currentVideoGames.Where(g => g.Contains(" ")).OrderBy(g => g).Select(g => g);

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}

Everything is the same as the previous method, except for the line in bold. This is using the extension method syntax. This syntax uses lambda expressions within each method to define the operation. For example, the lambda in the Where() method defines the condition (where a value contains a space). Just as in the query expression syntax, the letter used to indicate the value being evaluated in the lambda is arbitrary; I could have used v for video games.
While the results are the same (running this method produces the same output as the previous method using the query expression), you will see soon that the type of the result set is slightly different. For most
(if not practically all) scenarios, this difference doesn’t cause any issues, and the formats can be used interchangeably.

Once Again, Without LINQ
To be sure, LINQ is never mandatory. If you so choose, you could have found the same result set by forgoing LINQ altogether and making use of programming primitives such as if statements and for loops. Here is a method that yields the same result as the QueryOverStrings() method but in a much more verbose manner:
static void QueryOverStringsLongHand()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};
string[] gamesWithSpaces = new string[5];
for (int i = 0; i < currentVideoGames.Length; i++)
{
if (currentVideoGames[i].Contains(" "))
{
gamesWithSpaces[i] = currentVideoGames[i];
}
}
// Now sort them. Array.Sort(gamesWithSpaces);
// Print out the results.
foreach (string s in gamesWithSpaces)
{
if( s != null)
{
Console.WriteLine("Item: {0}", s);
}
}
Console.WriteLine();
}
While I am sure you can think of ways to tweak the previous method, the fact remains that LINQ queries
can be used to radically simplify the process of extracting new subsets of data from a source. Rather than building nested loops, complex if/else logic, temporary data types, and so on, the C# compiler will perform the dirty work on your behalf, once you create a fitting LINQ query.

Reflecting Over a LINQ Result Set
Now, assume the Program.cs file defines an additional helper function named ReflectOverQueryResults() that will print out various details of the LINQ result set (note the parameter is a System.Object to account for multiple types of result sets).

static void ReflectOverQueryResults(object resultSet, string queryType = "Query Expressions")
{
Console.WriteLine($" Info about your query using {queryType} "); Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name); Console.WriteLine("resultSet location: {0}", resultSet.GetType().Assembly.GetName().Name);
}

Update the core of the QueryOverStrings() method to the following:

// Build a query expression to find the items in the array
// that have an embedded space. IEnumerable subset =
from g in currentVideoGames where g.Contains(" ") orderby g
select g;

ReflectOverQueryResults(subset);

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}

When you run the application, you will see the subset variable is really an instance of the generic OrderedEnumerable<TElement, TKey> type (represented as OrderedEnumerable`2), which is an internal abstract type residing in the System.Linq.dll assembly.

Info about your query using Query Expressions resultSet is of type: OrderedEnumerable`2
resultSet location: System.Linq

Make the same change to the QueryOverStringsWithExtensionMethods() method, except for adding
"Extension Methods" for the second parameter.

// Build a query expression to find the items in the array
// that have an embedded space. IEnumerable subset =
currentVideoGames
.Where(g => g.Contains(" "))
.OrderBy(g => g)
.Select(g => g);

ReflectOverQueryResults(subset,"Extension Methods");

// Print out the results. foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}

When you run the application, you will see the subset variable is an instance of type SelectIPartitionIterator. If you remove Select(g=>g) from the query, you will be back to having an instance of type OrderedEnumerable<TElement, TKey>. What does this all mean? For most developers, not much (if anything). They both derive from IEnumerable, both can be iterated over in the same manner, and both can create a list or an array from their values.

Info about your query using Extension Methods resultSet is of type: SelectIPartitionIterator`2 resultSet location: System.Linq

LINQ and Implicitly Typed Local Variables
While the current sample program makes it relatively easy to determine that the result set can be captured as an enumeration of the string object (e.g., IEnumerable), I would guess that it is not clear that subset is really of type OrderedEnumerable<TElement, TKey>.
Given that LINQ result sets can be represented using a good number of types in various LINQ-centric namespaces, it would be tedious to define the proper type to hold a result set, because in many cases the underlying type may not be obvious or even directly accessible from your code base (and as you will see, in some cases the type is generated at compile time).
To further accentuate this point, consider the following additional helper method defined within the
Program.cs file:

static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};

// Print only items less than 10.
IEnumerable subset = from i in numbers where i < 10 select i;

foreach (int i in subset)
{
Console.WriteLine("Item: {0}", i);
}
ReflectOverQueryResults(subset);
}

In this case, the subset variable is a completely different underlying type. This time, the type implementing the IEnumerable interface is a low-level class named WhereArrayIterator.

Item: 1
Item: 2
Item: 3
Item: 8

Info about your query resultSet is of type: WhereArrayIterator`1 resultSet location: System.Linq

Given that the exact underlying type of a LINQ query is certainly not obvious, these first examples have represented the query results as an IEnumerable variable, where T is the type of data in the returned sequence (string, int, etc.). However, this is still rather cumbersome. To add insult to injury, given that IEnumerable extends the nongeneric IEnumerable interface, it would also be permissible to capture the result of a LINQ query as follows:

System.Collections.IEnumerable subset = from i in numbers
where i < 10 select i;

Thankfully, implicit typing cleans things up considerably when working with LINQ queries.

static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};

// Use implicit typing here…
var subset = from i in numbers where i < 10 select i;

// …and here.
foreach (var i in subset)
{
Console.WriteLine("Item: {0} ", i);
}
ReflectOverQueryResults(subset);
}

As a rule of thumb, you will always want to make use of implicit typing when capturing the results of a LINQ query. Just remember, however, that (in most cases) the real return value is a type implementing the generic IEnumerable interface.
Exactly what this type is under the covers (OrderedEnumerable<TElement, TKey>, WhereArrayIterator, etc.) is irrelevant and not necessary to discover. As shown in the previous code example, you can simply use the var keyword within a foreach construct to iterate over the fetched data.

LINQ and Extension Methods
Although the current example does not have you author any extension methods directly, you are in fact using them seamlessly in the background. LINQ query expressions can be used to iterate over data
containers that implement the generic IEnumerable interface. However, the System.Array class type (used to represent the array of strings and array of integers) does not implement this contract.

// The System.Array type does not seem to implement the
// correct infrastructure for query expressions! public abstract class Array : ICloneable, IList, IStructuralComparable, IStructuralEquatable
{

}

While System.Array does not directly implement the IEnumerable interface, it indirectly gains the required functionality of this type (as well as many other LINQ-centric members) via the static System.
Linq.Enumerable class type.
This utility class defines a good number of generic extension methods (such as Aggregate(), First(), Max(), etc.), which System.Array (and other types) acquires in the background. Thus, if you apply the dot operator on the currentVideoGames local variable, you will find a good number of members not found within the formal definition of System.Array.

The Role of Deferred Execution
Another important point regarding LINQ query expressions is that when they return a sequence, they are not actually evaluated until you iterate over the resulting sequence. Formally speaking, this is termed deferred execution. The benefit of this approach is that you can apply the same LINQ query multiple times to the same container and rest assured you are obtaining the latest and greatest results. Consider the following update to the QueryOverInts() method:

static void QueryOverInts()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };

// Get numbers less than ten.
var subset = from i in numbers where i < 10 select i;

// LINQ statement evaluated here!
foreach (var i in subset)
{
Console.WriteLine("{0} < 10", i);
}
Console.WriteLine();
// Change some data in the array. numbers[0] = 4;

// Evaluated again!
foreach (var j in subset)
{
Console.WriteLine("{0} < 10", j);
}

Console.WriteLine(); ReflectOverQueryResults(subset);
}

■ Note When a LINQ statement is selecting a single element (using First/FirstOrDefault, Single/SingleOrDefault, or any of the aggregation methods), the query is executed immediately. First, FirstOrDefault, Single, and SingleOrDefault are covered in the next section. The aggregation methods are covered later in this chapter.

If you were to execute the program yet again, you would find the following output. Notice that the second time you iterate over the requested sequence, you find an additional member, as you set the first item in the array to be a value less than ten.

1 < 10
2 < 10
3 < 10
8 < 10

4 < 10
1 < 10
2 < 10
3 < 10
8 < 10

One useful aspect of Visual Studio is that if you set a breakpoint before the evaluation of a LINQ query, you can view the contents during a debugging session. Simply locate your mouse cursor over the LINQ result set variable (subset in Figure 13-1). When you do, you will be given the option of evaluating the query at that time by expanding the Results View option.

Figure 13-1. Debugging LINQ expressions

DefaultIfEmpty (New 10.0)
New to C# 10, the DefaultIfEmpty() method returns the elements of the sequence or a default value if the sequence is empty. The query execution is deferred until the list is iterated. The following example shows DefaultIfEmpty() in action:

static void DefaultWhenEmpty()
{
Console.WriteLine("Default When Empty");
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };

//Returns all of the numbers
foreach (var i in numbers.DefaultIfEmpty(-1))
{
Console.Write($"{i},");
}
Console.WriteLine();
//Returns -1 since the sequence is empty
foreach (var i in (from i in numbers where i > 99 select i).DefaultIfEmpty(-1))
{

Console.Write($"{i},");
}
Console.WriteLine();
}

The Role of Immediate Execution
When you need to evaluate a LINQ expression resulting in a sequence from outside the confines of foreach logic, you can call any number of extension methods defined by the Enumerable type such as ToArray(), ToDictionary<TSource,TKey>(), and ToList(). These methods will cause a LINQ query to execute at the exact moment you call one of the extension methods to obtain a snapshot of the data. After you have done so, the snapshot of data may be independently manipulated.
Additionally, if you are seeking only a single element, the query is executed immediately.
First() returns the first member of the sequence (and should always be used with an OrderBy() or OrderByDescending()). FirstOrDefault() returns the default value for the type of item in the list if there aren’t any to return, such as when the original sequence is empty or the Where() clause filters out all elements. Single() also returns the first member of the sequence (based on the Orderby()/ OrderByDescending(), or element order if there isn’t an ordering clause). Like its similarly named counterpart, SingleOrDefault() returns the default value for the element type if there aren’t any items in the sequence (or all records are filtered out by a where clause). If no records are returned, First() and Single() throw an exception that no records were returned, while FirstOrDefault() and SingleOrDefault() simply return null. The difference between First()/FirstOrDefault() and Single()/SingleOr() is that Single()/SingleOrDefault() will throw an exception if more than one element is returned from the query.

static void ImmediateExecution()
{
Console.WriteLine(); Console.WriteLine("Immediate Execution");
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };

//get the first element in sequence order
int number = (from i in numbers select i).First(); Console.WriteLine("First is {0}", number);

//get the first in query order
number = (from i in numbers orderby i select i).First(); Console.WriteLine("First is {0}", number);

//get the one element that matches the query
number = (from i in numbers where i > 30 select i).Single(); Console.WriteLine("Single is {0}", number);

//Return null if nothing is returned
number = (from i in numbers where i > 99 select i).FirstOrDefault(); number = (from i in numbers where i > 99 select i).SingleOrDefault(); try
{
//Throw an exception if no records returned
number = (from i in numbers where i > 99 select i).First();
}

catch (Exception ex)
{

}

try
{

}

Console.WriteLine("An exception occurred: {0}", ex.Message);

//Throw an exception if no records returned
number = (from i in numbers where i > 99 select i).Single();

catch (Exception ex)
{

}

try
{

}

Console.WriteLine("An exception occurred: {0}", ex.Message);

//Throw an exception if more than one element passes the query number = (from i in numbers where i > 10 select i).Single();

catch (Exception ex)
{
Console.WriteLine("An exception occurred: {0}", ex.Message);
}
// Get data RIGHT NOW as int[].
int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray();

// Get data RIGHT NOW as List.
List subsetAsListOfInts =
(from i in numbers where i < 10 select i).ToList();
}

Notice that the entire LINQ expression is wrapped within parentheses to cast it into the correct underlying type (whatever that might be) to call the extension methods of Enumerable.
Also recall from Chapter 10 that when the C# compiler can unambiguously determine the type parameter of a generic, you are not required to specify the type parameter. Thus, you could also call ToArray() (or ToList() for that matter) as follows:

int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray();

The usefulness of immediate execution is obvious when you need to return the results of a LINQ query to an external caller. And, as luck would have it, this happens to be the next topic of this chapter.

Set Default for [First/Last/Single]OrDefault Methods (New 10)
The FirstOrDefault(), SingleOrDefault(), and LastOrDefault() methods have been updated in .NET 6/C# 10 to allow specification of the default value when the query doesn’t return any elements. The base version of these methods automatically sets the default value (0 for a number, null for class, etc.). Now you can programmatically set the default when no record is returned.

Take the following trivial example. The sample LINQ query doesn’t return any records. Instead of returning the default value of zero, each of the methods returns a different negative number.

static void SettingDefaults()
{
int[] numbers = Array.Empty();
var query = from i in numbers where i>100 select i; var number = query.FirstOrDefault(-1); Console.WriteLine(number);
number = query.SingleOrDefault(-2); Console.WriteLine(number);
number = query.LastOrDefault(-3); Console.WriteLine(number);
}

Returning the Result of a LINQ Query
It is possible to define a field within a class (or structure) whose value is the result of a LINQ query. To do so, however, you cannot use implicit typing (as the var keyword cannot be used for fields), and the target of the LINQ query cannot be instance-level data; therefore, it must be static. Given these limitations, you will seldom need to author code like the following:

class LINQBasedFieldsAreClunky
{
private static string[] currentVideoGames =
{"Morrowind", "Uncharted 2",
"Fallout 3", "Daxter", "System Shock 2"};

// Can’t use implicit typing here! Must know type of subset!
private IEnumerable subset = from g in currentVideoGames
where g.Contains(" ") orderby g
select g;

public void PrintGames()
{
foreach (var item in subset)
{
Console.WriteLine(item);
}
}
}

Often, LINQ queries are defined within the scope of a method or property. Moreover, to simplify your programming, the variable used to hold the result set will be stored in an implicitly typed local variable using the var keyword. Now, recall from Chapter 3 that implicitly typed variables cannot be used to define parameters, return values, or fields of a class or structure.
Given this point, you might wonder exactly how you could return a query result to an external caller.
The answer is: it depends. If you have a result set consisting of strongly typed data, such as an array of

strings or a List, you could abandon the use of the var keyword and use a proper IEnumerable or IEnumerable type (again, as IEnumerable extends IEnumerable). Consider the following example for a new console application named LinqRetValues:

Console.WriteLine(" LINQ Return Values \n"); IEnumerable subset = GetStringSubset();

foreach (string item in subset)
{
Console.WriteLine(item);
}

Console.ReadLine();

static IEnumerable GetStringSubset()
{
string[] colors = {"Light Red", "Green", "Yellow", "Dark Red", "Red", "Purple"};
// Note subset is an IEnumerable-compatible object.
IEnumerable theRedColors = from c in colors where c.Contains("Red") select c; return theRedColors;
}

The results are as expected.

Light Red Dark Red Red

Returning LINQ Results via Immediate Execution
This example works as expected, only because the return value of GetStringSubset() and the LINQ query within this method has been strongly typed. If you used the var keyword to define the subset variable, it would be permissible to return the value only if the method is still prototyped to return IEnumerable (and if the implicitly typed local variable is in fact compatible with the specified return type).
Because it is a bit inconvenient to operate on IEnumerable, you could use immediate execution. For example, rather than returning IEnumerable, you could simply return a string[], if you transform the sequence to a strongly typed array. Consider this new method of the Program.cs file, which does this very thing:

static string[] GetStringSubsetAsArray()
{
string[] colors = {"Light Red", "Green", "Yellow", "Dark Red", "Red", "Purple"}; var theRedColors = from c in colors where c.Contains("Red") select c;
// Map results into an array. return theRedColors.ToArray();
}

With this, the caller can be blissfully unaware that their result came from a LINQ query and simply work with the array of strings as expected. Here’s an example:

foreach (string item in GetStringSubsetAsArray())
{
Console.WriteLine(item);
}

Immediate execution is also critical when attempting to return to the caller the results of a LINQ projection. You’ll examine this topic a bit later in the chapter. Next up, let’s look at how to apply LINQ queries to generic and nongeneric collection objects.

Applying LINQ Queries to Collection Objects
Beyond pulling results from a simple array of data, LINQ query expressions can also manipulate data within members of the System.Collections.Generic namespace, such as the List type. Create a new Console Application project named LinqOverCollections, and define a basic Car class that maintains a current speed, color, make, and pet name, as shown in the following code:

namespace LinqOverCollections; class Car
{
public string PetName {get; set;} = ""; public string Color {get; set;} = ""; public int Speed {get; set;}
public string Make {get; set;} = "";
}

Now, within your top-level statements, define a local List variable of type Car, and make use of object initialization syntax to fill the list with a handful of new Car objects.

using System.Collections; using LinqOverCollections;

Console.WriteLine(" LINQ over Generic Collections \n");

// Make a List<> of Car objects. List myCars = new List() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};

Console.ReadLine();

Accessing Contained Subobjects
Applying a LINQ query to a generic container is no different from doing so with a simple array, as LINQ to Objects can be used on any type implementing IEnumerable. This time, your goal is to build a query expression to select only the Car objects within the myCars list, where the speed is greater than 55.
After you get the subset, you will print out the name of each Car object by calling the PetName property. Assume you have the following helper method (taking a List parameter), which is called from the top- level statements:

static void GetFastCars(List myCars)
{
// Find all Car objects in the List<>, where the Speed is
// greater than 55.
var fastCars = from c in myCars where c.Speed > 55 select c;

foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}

Notice that your query expression is grabbing only those items from the List where the Speed property is greater than 55. If you run the application, you will find that Henry and Daisy are the only two items that match the search criteria.
If you want to build a more complex query, you might want to find only the BMWs that have a Speed
value greater than 90. To do so, simply build a compound Boolean statement using the C# && operator.

static void GetFastBMWs(List myCars)
{
// Find the fast BMWs!
var fastCars = from c in myCars where c.Speed > 90 && c.Make == "BMW" select c; foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}

In this case, the only pet name printed out is Henry.

Applying LINQ Queries to Nongeneric Collections
Recall that the query operators of LINQ are designed to work with any type implementing IEnumerable (either directly or via extension methods). Given that System.Array has been provided with such necessary infrastructure, it might surprise you that the legacy (nongeneric) containers within System.Collections have not. Thankfully, it is still possible to iterate over data contained within nongeneric collections using the generic Enumerable.OfType() extension method.
When calling OfType() from a nongeneric collection object (such as the ArrayList), simply specify the type of item within the container to extract a compatible IEnumerable object. In code, you can store this data point using an implicitly typed variable.
Consider the following new method, which fills an ArrayList with a set of Car objects (be sure to import the System.Collections namespace into your Program.cs file):

static void LINQOverArrayList()
{
Console.WriteLine(" LINQ over ArrayList ");
// Here is a nongeneric collection of cars. ArrayList myCars = new ArrayList() {
new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}
};
// Transform ArrayList into an IEnumerable-compatible type. var myCarsEnum = myCars.OfType();
// Create a query expression targeting the compatible type.
var fastCars = from c in myCarsEnum where c.Speed > 55 select c; foreach (var car in fastCars)
{
Console.WriteLine("{0} is going too fast!", car.PetName);
}
}
Like the previous examples, this method, when called from the top-level statements, will display only
the names Henry and Daisy, based on the format of the LINQ query.

Filtering Data Using OfType()
As you know, nongeneric types can contain any combination of items, as the members of these containers (again, such as the ArrayList) are prototyped to receive System.Objects. For example, assume an ArrayList contains a variety of items, only a subset of which are numerical. If you want to obtain a subset that contains only numerical data, you can do so using OfType() since it filters out each element whose type is different from the given type during the iterations.

static void OfTypeAsFilter()
{
// Extract the ints from the ArrayList. ArrayList myStuff = new ArrayList();
myStuff.AddRange(new object[] { 10, 400, 8, false, new Car(), "string data" }); var myInts = myStuff.OfType();

// Prints out 10, 400, and 8. foreach (int i in myInts)
{
Console.WriteLine("Int value: {0}", i);
}
}
At this point, you have had a chance to apply LINQ queries to arrays, generic collections, and
nongeneric collections. These containers held both C# primitive types (integers, string data) as well as custom classes. The next task is to learn about many additional LINQ operators that can be used to build more complex and useful queries.

Investigating the C# LINQ Query Operators
C# defines a good number of query operators out of the box. Table 13-2 documents some of the more commonly used query operators. In addition to the partial list of operators shown in Table 13-2, the System.Linq.Enumerable class provides a set of methods that do not have a direct C# query operator shorthand notation but are instead exposed as extension methods. These generic methods can be called to transform a result set in various manners (Reverse<>(), ToArray<>(), ToList<>(), etc.). Some are used to extract singletons from a result set, others perform various set operations (Distinct<>(), Union<>(), Intersect<>(), etc.), and still others aggregate results (Count<>(), Sum<>(), Min<>(), Max<>(), etc.).

Table 13-2. Common LINQ Query Operators

Query Operators Meaning in Life
from, in Used to define the backbone for any LINQ expression, which allows you to extract a subset of data from a fitting container.
where Used to define a restriction for which items to extract from a container.
select Used to select a sequence from the container.
join, on, equals, into Performs joins based on specified key. Remember, these “joins” do not need to have anything to do with data in a relational database.
orderby, ascending, descending Allows the resulting subset to be ordered in ascending or descending order.
groupby Yields a subset with data grouped by a specified value.

To begin digging into more intricate LINQ queries, create a new Console Application project named FunWithLinqExpressions. Next, you need to define an array or collection of some sample data. For this project, you will make an array of ProductInfo objects, defined in the following code:

namespace FunWithLinqExpressions; class ProductInfo
{
public string Name {get; set;} = "";
public string Description {get; set;} = ""; public int NumberInStock {get; set;} = 0;

public override string ToString()
=> $"Name={Name}, Description={Description}, Number in Stock={NumberInStock}";
}

Now populate an array with a batch of ProductInfo objects within your calling code.

Console.WriteLine(" Fun with Query Expressions \n");

// This array will be the basis of our testing… ProductInfo[] itemsInStock = new[] {
new ProductInfo{ Name = "Mac’s Coffee", Description = "Coffee with TEETH", NumberInStock = 24},

new ProductInfo{ Name = "Milk Maid Milk", Description = "Milk cow’s love", NumberInStock = 100},
new ProductInfo{ Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberInStock = 120},
new ProductInfo{ Name = "Crunchy Pops", Description = "Cheezy, peppery goodness", NumberInStock = 2},
new ProductInfo{ Name = "RipOff Water", Description = "From the tap to your wallet", NumberInStock = 100},
new ProductInfo{ Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberInStock = 73}
};

// We will call various methods here! Console.ReadLine();

Basic Selection Syntax
Because the syntactical correctness of a LINQ query expression is validated at compile time, you need to remember that the ordering of these operators is critical. In the simplest terms, every LINQ query expression is built using the from, in, and select operators. Here is the general template to follow:

var result =
from matchingItem in container select matchingItem;

The item after the from operator represents an item that matches the LINQ query criteria, which can be named anything you choose. The item after the in operator represents the data container to search (an array, collection, XML document, etc.).
Here is a simple query, doing nothing more than selecting every item in the container (similar in behavior to a database Select * SQL statement). Consider the following:

static void SelectEverything(ProductInfo[] products)
{
// Get everything!
Console.WriteLine("All product details:");
var allProducts = from p in products select p;

foreach (var prod in allProducts)
{
Console.WriteLine(prod.ToString());
}
}

To be honest, this query expression is not entirely useful, given that your subset is identical to that of the data in the incoming parameter. If you want, you could extract only the Name values of each car using the following selection syntax:

static void ListProductNames(ProductInfo[] products)
{
// Now get only the names of the products. Console.WriteLine("Only product names:");

var names = from p in products select p.Name;

foreach (var n in names)
{
Console.WriteLine("Name: {0}", n);
}
}

Obtaining Subsets of Data
To obtain a specific subset from a container, you can use the where operator. When doing so, the general template now becomes the following code:

var result = from item
in container
where BooleanExpression select item;

Notice that the where operator expects an expression that resolves to a Boolean. For example, to extract from the ProductInfo[] argument only the items that have more than 25 items on hand, you could author the following code:

static void GetOverstock(ProductInfo[] products)
{
Console.WriteLine("The overstock items!");

// Get only the items where we have more than
// 25 in stock. var overstock =
from p
in products
where p.NumberInStock > 25 select p;

foreach (ProductInfo c in overstock)
{
Console.WriteLine(c.ToString());
}
}

As shown earlier in this chapter, when you are building a where clause, it is permissible to make use of any valid C# operators to build complex expressions. For example, recall this query that extracts only the BMWs going at least 100 MPH:

// Get BMWs going at least 100 MPH. var onlyFastBMWs =
from c
in myCars
where c.Make == "BMW" && c.Speed >= 100 select c;

Paging Data
If you need to get a certain number of records, you can use the Take()/TakeWhile()/TakeLast() and Skip()/SkipWhile()/SkipLast() methods. The first set of methods returns the specified number of records (Take()), all of the records where the condition is true (TakeWhile()), or the last specified number of records (TakeLast()). The second set of methods passes over the specified number of records (Skip()), skips all
of the records where the condition is true (SkipWhile()), or skips the last specified number of records (SkipLast()).
These methods are exposed through the IEnumerable interface, so while you can’t use the paging operators on the LINQ statements directly, you can use them on the result of the LINQ statement.
Begin by creating a new method named PagingWithLinq(). The first example will take the first three records from the list and then pass the result to a local function for display. The paging methods (like the where clause) participate in deferred execution, so the return type is also an IEnumerable. Here is the method with the first example and local helper function:

static void PagingWithLINQ(ProductInfo[] products)
{
Console.WriteLine("Paging Operations");

IEnumerable list = (from p in products select p).Take(3); OutputResults("The first 3",list);

static void OutputResults(string message, IEnumerable products)
{
Console.WriteLine(message);
foreach (ProductInfo c in products)
{
Console.WriteLine(c.ToString());
}
}
}

The TakeWhile() method takes records as long as a condition is true. The condition is passed into the method as a lambda expression, like this:

list = (from p in products select p).TakeWhile(x=>x.NumberInStock>20); OutputResults("All while number in stock > 20",list);

The preceding code returns the first three in the list, as the fourth record (“Crunchy Pops”) has only two items in stock. It’s important to note that the method stops taking records when the condition fails, even though there are two more items that would have passed the condition. If you need to take all the items into consideration, then add an orderby clause to the list before calling TakeWhile().
The TakeLast() method takes the last specified number of records.

list = (from p in products select p).TakeLast(2); OutputResults("The last 2",list);

The Skip() and SkipWhile() methods work in the same manner, only skipping records instead of taking records. The next example skips the first three records and then returns the rest:

list = (from p in products select p).Skip(3); OutputResults("Skipping the first 3",list);

To skip the records where the number in stock is greater than 20, use the following code. Note that the same issue of sorting exists with SkipWhile() as TakeWhile(). Both methods are best used when the records are sorted accordingly.

list = (from p in products select p).SkipWhile(x=>x.NumberInStock>20); OutputResults("Skip while number in stock > 20",list);

The SkipLast() method takes all but the last specified number of records:

list = (from p in products select p).SkipLast(2); OutputResults("All but the last 2",list);

These methods can be combined to truly “page” the data. To skip three records and take two, execute the following code:

list = (from p in products select p).Skip(3).Take(2); OutputResults("Skip 3 take 2",list);

Paging Data with Ranges (New 10.0)
Support for using ranges in the Take() method has been added in .NET 6/C# 10, enabling paging without needing the Take() and Skip() methods used together. Note that the TakeWhile() and SkipWhile() methods have not been updated to accept ranges.
Here is a new method named PagingWithRanges() that repeats the calls to Take() and Skip() from the previous example, using ranges to retrieve the same data:

static void PagingWithRanges(ProductInfo[] products)
{
Console.WriteLine("Paging Operations");

IEnumerable list = (from p in products select p).Take(..3); OutputResults("The first 3",list);

list = (from p in products select p).Take(3..); OutputResults("Skipping the first 3",list);

list = (from p in products select p).Take(^2..); OutputResults("The last 2",list);

list = (from p in products select p).Take(3..5); OutputResults("Skip 3 take 2",list);

list = (from p in products select p).Take(..^2); OutputResults("Skip the last 2",list);

static void OutputResults(string message, IEnumerable products)
{
Console.WriteLine(message);
foreach (ProductInfo c in products)
{

Console.WriteLine(c.ToString());
}
}
}

Paging Data with Chunks (New 10.0)
Chunk() is another new method for paging has been added in .NET 6/C# 10. This method takes one parameter (size) and then splits the source into an Enumerable of Enumerables. For example, if we take the ProductInfo list and apply Chunk(2), the return value is a list of three lists, each inner list containing two records. If the list can’t be split evenly, the last list will hold fewer records.
Here is a new method named PagingWithChunks() that demonstrates the Chunk() method:

static void PagingWithChunks(ProductInfo[] products)
{
Console.WriteLine("Chunking Operations");

IEnumerable<ProductInfo[]> chunks = products.Chunk(size:2); var counter = 0;
foreach (var chunk in chunks)
{
OutputResults($"Chunk #{++counter}",chunk);
}
static void OutputResults(string message, IEnumerable products)
{
Console.WriteLine(message);
foreach (ProductInfo c in products)
{
Console.WriteLine(c.ToString());
}
}
}

Projecting New Data Types
It is also possible to project new forms of data from an existing data source. Let’s assume you want to take the incoming ProductInfo[] parameter and obtain a result set that accounts only for the name and description of each item. To do so, you can define a select statement that dynamically yields a new anonymous type.

static void GetNamesAndDescriptions(ProductInfo[] products)
{
Console.WriteLine("Names and Descriptions:"); var nameDesc =
from p
in products
select new { p.Name, p.Description };

foreach (var item in nameDesc)
{

// Could also use Name and Description properties
// directly. Console.WriteLine(item.ToString());
}
}

Always remember that when you have a LINQ query that makes use of a projection, you have no way of knowing the underlying data type, as this is determined at compile time. In these cases, the var keyword is mandatory. As well, recall that you cannot create methods with implicitly typed return values. Therefore, the following method would not compile:

static var GetProjectedSubset(ProductInfo[] products)
{
var nameDesc =
from p in products select new { p.Name, p.Description }; return nameDesc; // Nope!
}

When you need to return projected data to a caller, one approach is to transform the query result into a System.Array object using the ToArray() extension method. Thus, if you were to update your query expression as follows:

// Return value is now an Array.
static Array GetProjectedSubset(ProductInfo[] products)
{
var nameDesc =
from p in products select new { p.Name, p.Description };

// Map set of anonymous objects to an Array object. return nameDesc.ToArray();
}

you could invoke and process the data as follows:

Array objs = GetProjectedSubset(itemsInStock); foreach (object o in objs)
{
Console.WriteLine(o); // Calls ToString() on each anonymous object.
}

Note that you must use a literal System.Array object and cannot use the C# array declaration syntax, given that you don’t know the underlying type because you are operating on a compiler-generated anonymous class! Also note that you are not specifying the type parameter to the generic ToArray() method, as you once again don’t know the underlying data type until compile time, which is too late for your purposes.
The obvious problem is that you lose any strong typing, as each item in the Array object is assumed to be of type Object. Nevertheless, when you need to return a LINQ result set that is the result of a projection operation to an anonymous type, transforming the data into an Array type (or another suitable container via other members of the Enumerable type) is mandatory.

Projecting to Different Data Types
In addition to projecting into anonymous types, you can project the results of your LINQ query into another concrete type. This allows for static typing and using IEnumerable as the result set. To start, create a smaller version of the ProductInfo class.

namespace FunWithLinqExpressions; class ProductInfoSmall
{
public string Name {get; set;} = "";
public string Description {get; set;} = ""; public override string ToString()
=> $"Name={Name}, Description={Description}";
}

The next change is to project the query results into a collection of ProductInfoSmall objects, instead of anonymous types. Add the following method to your class:

static void GetNamesAndDescriptionsTyped( ProductInfo[] products)
{
Console.WriteLine("Names and Descriptions:"); IEnumerable nameDesc =
from p
in products
select new ProductInfoSmall
{ Name=p.Name, Description=p.Description };

foreach (ProductInfoSmall item in nameDesc)
{
Console.WriteLine(item.ToString());
}
}

With LINQ projections, you have choices for which method you use (anonymous or strong-typed objects). Which decision you make depends entirely on your business need.

Obtaining Counts Using Enumerable
When you are projecting new batches of data, you may need to discover exactly how many items have been returned into the sequence. Any time you need to determine the number of items returned from a LINQ query expression, simply use the Count() extension method of the Enumerable class. For example, the following method will find all string objects in a local array that have a length greater than six characters:

static void GetCountFromQuery()
{
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Get count from the query.
int numb = (from g in currentVideoGames where g.Length > 6 select g).Count();

// Print out the number of items.
Console.WriteLine("{0} items honor the LINQ query.", numb);
}

Obtaining Nonenumerated Counts (New 10.0)
Introduced in .NET 6/C# 10, the TryGetNonEnumeratedCount() method attempts to get the total count of an IEnumerable without actually enumerating the list. If the list must be enumerated over to get the count, the method fails.
The following code demonstrates an “easy” count that doesn’t cause a performance issue by enumerating the entire list:

static void GetUnenumeratedCount(ProductInfo[] products)
{
Console.WriteLine("Get Unenumeratord Count"); IEnumerable query = from p in products select p; var result = query.TryGetNonEnumeratedCount(out int count); if (result)
{
Console.WriteLine($"Total count:{count}");
}
else
{
Console.WriteLine("Try Get Count Failed");
}
}

The following updated code fails when calling the GetProducts() local function, since the yield return must be enumerated:

static void GetUnenumeratedCount(ProductInfo[] products)
{
Console.WriteLine("Get Unenumeratord Count");
//omitted for brevity
var newResult = GetProduct(products).TryGetNonEnumeratedCount(out int newCount); if (newResult)
{
Console.WriteLine($"Total count:{newCount}");
}
else
{
Console.WriteLine("Try Get Count Failed");
}
static IEnumerable GetProduct(ProductInfo[] products)
{
for (int i = 0;i < products.Count();i++)
{
yield return products[i];
}
}
}

Reversing Result Sets
You can reverse the items within a result set quite simply using the Reverse() extension method of the Enumerable class. For example, the following method selects all items from the incoming ProductInfo[] parameter, in reverse:

static void ReverseEverything(ProductInfo[] products)
{
Console.WriteLine("Product in reverse:");
var allProducts = from p in products select p; foreach (var prod in allProducts.Reverse())
{
Console.WriteLine(prod.ToString());
}
}

Sorting Expressions
As you have seen in this chapter’s initial examples, a query expression can take an orderby operator to sort items in the subset by a specific value. By default, the order will be ascending; thus, ordering by a string would be alphabetical, ordering by numerical data would be lowest to highest, and so forth. If you need to view the results in descending order, simply include the descending operator. Ponder the following method:

static void AlphabetizeProductNames(ProductInfo[] products)
{
// Get names of products, alphabetized.
var subset = from p in products orderby p.Name select p;

Console.WriteLine("Ordered by Name:"); foreach (var p in subset)
{
Console.WriteLine(p.ToString());
}
}

Although ascending order is the default, you can make your intentions clear by using the ascending
operator.

var subset = from p in products orderby p.Name ascending select p;

If you want to get the items in descending order, you can do so via the descending operator.

var subset = from p in products orderby p.Name descending select p;

LINQ As a Better Venn Diagramming Tool
The Enumerable class supports a set of extension methods that allows you to use two (or more) LINQ queries as the basis to find unions, differences, concatenations, and intersections of data. First, consider the Except() extension method, which will return a LINQ result set that contains the difference between two containers, which, in this case, is the value Yugo.

static void DisplayDiff()
{
List myCars =
new List {"Yugo", "Aztec", "BMW"}; List yourCars =
new List{"BMW", "Saab", "Aztec" };

var carDiff =
(from c in myCars select c)
.Except(from c2 in yourCars select c2);

Console.WriteLine("Here is what you don’t have, but I do:"); foreach (string s in carDiff)
{
Console.WriteLine(s); // Prints Yugo.
}
}

The Intersect() method will return a result set that contains the common data items in a set of containers. For example, the following method returns the sequence Aztec and BMW:

static void DisplayIntersection()
{
List myCars = new List { "Yugo", "Aztec", "BMW" }; List yourCars = new List { "BMW", "Saab", "Aztec" };

// Get the common members. var carIntersect =
(from c in myCars select c)
.Intersect(from c2 in yourCars select c2);

Console.WriteLine("Here is what we have in common:"); foreach (string s in carIntersect)
{
Console.WriteLine(s); // Prints Aztec and BMW.
}
}

The Union() method, as you would guess, returns a result set that includes all members of a batch of LINQ queries. Like any proper union, you will not find repeating values if a common member appears more than once. Therefore, the following method will print out the values Yugo, Aztec, BMW, and Saab:

static void DisplayUnion()
{
List myCars =
new List { "Yugo", "Aztec", "BMW" }; List yourCars =
new List { "BMW", "Saab", "Aztec" };

// Get the union of these containers. var carUnion =

(from c in myCars select c)
.Union(from c2 in yourCars select c2);

Console.WriteLine("Here is everything:"); foreach (string s in carUnion)
{
Console.WriteLine(s); // Prints all common members.
}
}

Finally, the Concat() extension method returns a result set that is a direct concatenation of LINQ result sets. For example, the following method prints out the results Yugo, Aztec, BMW, BMW, Saab, and Aztec:

static void DisplayConcat()
{
List myCars =
new List { "Yugo", "Aztec", "BMW" }; List yourCars =
new List { "BMW", "Saab", "Aztec" };

var carConcat =
(from c in myCars select c)
.Concat(from c2 in yourCars select c2);

// Prints:
// Yugo Aztec BMW BMW Saab Aztec. foreach (string s in carConcat)
{
Console.WriteLine(s);
}
}

Venn Diagramming with Selectors (New 10.0)
A new set of methods was introduced in .NET 6/C# 10 that adds the ability to use a selector when finding unions, differences, and intersections of data. The selectors use a specific property of the objects in the lists to determine the action to perform.
The ExceptBy() extension method uses the selector to remove the records from the first set where the value of the selector exists in the second set. The following method creates two lists of tuples and uses the Age property for the selector. Both Claire and Pat have the same age as Lindsey, but Age values for
Francis and Ashley are not in the second list. So the result of the ExceptBy() extension method is Francis
and Ashley:

static void DisplayDiffBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.ExceptBy(second.Select(x=>x.Age), product => product.Age); // } Console.WriteLine("Except for by selector:");
foreach (var item in result)

{
Console.WriteLine(item); // { ("Francis", 20), ("Ashley", 40) }
}
}

The IntersectBy() method will return a result set that contains the common data items in a set of containers based on the selector. The following method returns the tuple Lindsey. Note that even though Claire and Pat also have the same Age value as Lindsey, they are not returned since the IntersectBy() method returns only one result per selector value.

static void DisplayIntersectionBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.IntersectBy(second.Select(x=>x.Age), person => person.Age); Console.WriteLine("Intersection by selector:");
foreach (var item in result)
{
Console.WriteLine(item); // { ("Lindsey", 30) }
}
}

Reversing the call between the first and second lists still produces one result, which is the first tuple in the second list with the selector value of 30.

var result = second.IntersectBy(first.Select(x=>x.Age), person => person.Age);
// returns (“Claire”,30)

The UnionBy() method returns a result set that includes all the values for the selector, and the first member of the combined lists that have a value that matches the list. In the following method, notice that while each of the Age values is represented in the result, Claire and Pat are not, since their Age is already represented by Lindsey:

static void DisplayUnionBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.UnionBy(second, person => person.Age);
Console.WriteLine("Union by selector:"); foreach (var item in result)
{
Console.WriteLine(item); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40),
("Drew", 33) };
}
}

These new methods, ExceptBy(), IntersectBy(), and UnionBy(), add a lot of power to your list arsenal, but as you saw, they might not always do what you expect them to do.

Removing Duplicates
When you call the Concat() extension method, you could very well end up with redundant entries in the fetched result, which could be exactly what you want in some cases. However, in other cases, you might want to remove duplicate entries in your data. To do so, simply call the Distinct() extension method, as shown here:

static void DisplayConcatNoDups()
{
List myCars =
new List { "Yugo", "Aztec", "BMW" }; List yourCars =
new List { "BMW", "Saab", "Aztec" };

var carConcat =
(from c in myCars select c)
.Concat(from c2 in yourCars select c2);

// Prints:
// Yugo Aztec BMW Saab.
foreach (string s in carConcat.Distinct())
{
Console.WriteLine(s);
}
}

Removing Duplicates with Selectors (New 10.0)
Another new method in .NET 6/C# 10 for working with lists is the DistinctBy() method. This method, like its cousins UnionBy(), IntersectBy(), and ExceptBy(), uses a selector to perform its function. The
following example gets the distinct records by Age. Notice that the final list only includes Lindsey of the three tuples that have 30 for their Age value. The accepted tuple is the one that appears first in the list.

static void DisplayConcatNoDupsBySelector()
{
var first = new (string Name, int Age)[] { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40) };
var second = new (string Name, int Age)[] { ("Claire", 30), ("Pat", 30), ("Drew", 33) }; var result = first.Concat(second).DistinctBy(x=>x.Age);
Console.WriteLine("Distinct by selector:"); foreach (var item in result)
{
Console.WriteLine(item); // { ("Francis", 20), ("Lindsey", 30), ("Ashley", 40),
("Drew", 33) };
}
}

LINQ Aggregation Operations
LINQ queries can also be designed to perform various aggregation operations on the result set. The Count() extension method is one such aggregation example. Other possibilities include obtaining an average, maximum, minimum, or sum of values using the Max(), Min(), Average(), or Sum() members of the Enumerable class. Here is a simple example:

static void AggregateOps()
{
double[] winterTemps = { 2.0, -21.3, 8, -4, 0, 8.2 };

// Various aggregation examples. Console.WriteLine("Max temp: {0}",
(from t in winterTemps select t).Max());

Console.WriteLine("Min temp: {0}",
(from t in winterTemps select t).Min());

Console.WriteLine("Average temp: {0}",
(from t in winterTemps select t).Average());

Console.WriteLine("Sum of all temps: {0}", (from t in winterTemps select t).Sum());
}

Aggregation with Selectors (New 10.0)
Two new aggregate functions introduced in .NET 6/C# 10 are MaxBy() and MinBy(). Using the ProductInfo
list from earlier, here is a method that gets the max and min, using the NumberInStock property:

static void AggregateOpsBySelector(ProductInfo[] products)
{
Console.WriteLine("Max by In Stock: {0}", products.MaxBy(x=>x.NumberInStock)); Console.WriteLine("Min temp: {0}", products.MinBy(x=>x.NumberInStock));
}

The Internal Representation of LINQ Query Statements
While this is not a complete LINQ reference, the previous examples in this chapter should give you enough knowledge to feel comfortable with the process of building LINQ query expressions. You will see further examples later in this text, especially in the Entity Framework Core chapters. To wrap up your first look at LINQ, the remainder of this chapter will dive into the details between the C# LINQ query operators and the underlying object model.
At this point, you have been introduced to the process of building query expressions using various C# query operators (such as from, in, where, orderby, and select). Also, you discovered that some functionality of the LINQ to Objects API can be accessed only when calling extension methods of the Enumerable class.
The truth of the matter, however, is that when LINQ queries are compiled, the C# compiler translates all C# LINQ operators into calls on methods of the Enumerable class.
A great many of the methods of Enumerable have been prototyped to take delegates as arguments. Many methods require a generic delegate named Func<>, which was introduced to you during your examination of

generic delegates in Chapter 10. Consider the Where() method of Enumerable, which is called on your behalf when you use the C# where LINQ query operator.

// Overloaded versions of the Enumerable.Where() method.
// Note the second parameter is of type System.Func<>.
public static IEnumerable Where( this IEnumerable source, System.Func<TSource,int,bool> predicate)

public static IEnumerable Where( this IEnumerable source, System.Func<TSource,bool> predicate)

The Func<> delegate (as the name implies) represents a pattern for a given function with a set of up to 16 arguments and a return value. If you were to examine this type using the Visual Studio Object Browser, you would notice various forms of the Func<> delegate. Here’s an example:

// The various formats of the Func<> delegate.
public delegate TResult Func<T1,T2,T3,T4,TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4) public delegate TResult Func<T1,T2,T3,TResult>(T1 arg1, T2 arg2, T3 arg3)
public delegate TResult Func<T1,T2,TResult>(T1 arg1, T2 arg2) public delegate TResult Func<T1,TResult>(T1 arg1)
public delegate TResult Func()

Given that many members of System.Linq.Enumerable demand a delegate as input, when invoking them, you can either manually create a new delegate type and author the necessary target methods, use a C# anonymous method, or define a proper lambda expression. Regardless of which approach you take, the result is identical.
While it is true that using C# LINQ query operators is far and away the simplest way to build a LINQ query expression, let’s walk through each of these possible approaches, just so you can see the connection between the C# query operators and the underlying Enumerable type.

Building Query Expressions with Query Operators (Revisited)
To begin, create a new Console Application project named LinqUsingEnumerable. The Program.cs file will define a series of static helper methods (each of which is called within the top-level statements) to illustrate the various manners in which you can build LINQ query expressions.
The first method, QueryStringsWithOperators(), offers the most straightforward way to build a query expression and is identical to the code shown in the LinqOverArray example earlier in this chapter.

static void QueryStringWithOperators()
{
Console.WriteLine(" Using Query Operators ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

var subset = from game in currentVideoGames
where game.Contains(" ") orderby game select game;

foreach (string s in subset)
{
Console.WriteLine("Item: {0}", s);
}
}

The obvious benefit of using C# query operators to build query expressions is that the Func<> delegates and calls on the Enumerable type are abstracted away from your code, as it is the job of the C# compiler to perform this translation. To be sure, building LINQ expressions using various query operators (from, in, where, or orderby) is the most common and straightforward approach.

Building Query Expressions Using the Enumerable Type and Lambda Expressions
Keep in mind that the LINQ query operators used here are simply shorthand versions for calling various extension methods defined by the Enumerable type. Consider the following
QueryStringsWithEnumerableAndLambdas() method, which is processing the local string array now making direct use of the Enumerable extension methods:

static void QueryStringsWithEnumerableAndLambdas()
{
Console.WriteLine(" Using Enumerable / Lambda Expressions ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build a query expression using extension methods
// granted to the Array via the Enumerable type. var subset = currentVideoGames
.Where(game => game.Contains(" "))
.OrderBy(game => game).Select(game => game);

// Print out the results. foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

Here, you begin by calling the Where() extension method on the currentVideoGames string array.
Recall that the Array class receives this via an extension method granted by Enumerable. The Enumerable. Where() method requires a System.Func<T1, TResult> delegate parameter. The first type parameter of this delegate represents the IEnumerable-compatible data to process (an array of strings in this case), while the second type parameter represents the method result data, which is obtained from a single statement fed into the lambda expression.

The return value of the Where() method is hidden from view in this code example, but under the covers you are operating on an Enumerable type. From this object, you call the generic OrderBy() method, which also requires a Func<> delegate parameter. This time, you are simply passing each item in turn via a fitting lambda expression. The result of calling OrderBy() is a new ordered sequence of the initial data.
Finally, you call the Select() method off the sequence returned from OrderBy(), which results in the final set of data that is stored in an implicitly typed variable named subset.
To be sure, this “longhand” LINQ query is a bit more complex to tease apart than the previous C# LINQ query operator example. Part of the complexity is, no doubt, due to the chaining together of calls using the dot operator. Here is the same query, with each step broken into discrete chunks (as you might guess, you could break down the overall query in various manners):

static void QueryStringsWithEnumerableAndLambdas2()
{
Console.WriteLine(" Using Enumerable / Lambda Expressions ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Break it down!
var gamesWithSpaces = currentVideoGames.Where(game => game.Contains(" ")); var orderedGames = gamesWithSpaces.OrderBy(game => game);
var subset = orderedGames.Select(game => game);

foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

As you might agree, building a LINQ query expression using the methods of the Enumerable class directly is much more verbose than making use of the C# query operators. As well, given that the methods of Enumerable require delegates as parameters, you will typically need to author lambda expressions to allow the input data to be processed by the underlying delegate target.

Building Query Expressions Using the Enumerable Type and Anonymous Methods
Given that C# lambda expressions are simply shorthand notations for working with anonymous methods, consider the third query expression created within the QueryStringsWithAnonymousMethods() helper function, shown here:

static void QueryStringsWithAnonymousMethods()
{
Console.WriteLine(" Using Anonymous Methods ");

string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build the necessary Func<> delegates using anonymous methods.

Func<string, bool> searchFilter = delegate(string game) { return game.Contains(" "); }; Func<string, string> itemToProcess = delegate(string s) { return s; };

// Pass the delegates into the methods of Enumerable.
var subset = currentVideoGames.Where(searchFilter).OrderBy(itemToProcess). Select(itemToProcess);

// Print out the results. foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

This iteration of the query expression is even more verbose, because you are manually creating the Func<> delegates used by the Where(), OrderBy(), and Select() methods of the Enumerable class. On the plus side, the anonymous method syntax does keep all the delegate processing contained within a single method definition. Nevertheless, this method is functionally equivalent to the
QueryStringsWithEnumerableAndLambdas() and QueryStringsWithOperators() methods created in the previous sections.

Building Query Expressions Using the Enumerable Type and Raw Delegates
Finally, if you want to build a query expression using the verbose approach, you could avoid the use of lambdas/anonymous method syntax and directly create delegate targets for each Func<> type. Here is the final iteration of your query expression, modeled within a new class type named VeryComplexQueryExpression:

class VeryComplexQueryExpression
{
public static void QueryStringsWithRawDelegates()
{
Console.WriteLine(" Using Raw Delegates ");
string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"};

// Build the necessary Func<> delegates. Func<string, bool> searchFilter =
new Func<string, bool>(Filter); Func<string, string> itemToProcess =
new Func<string,string>(ProcessItem);

// Pass the delegates into the methods of Enumerable. var subset =
currentVideoGames
.Where(searchFilter)
.OrderBy(itemToProcess)
.Select(itemToProcess);

// Print out the results. foreach (var game in subset)
{
Console.WriteLine("Item: {0}", game);
}
Console.WriteLine();
}

// Delegate targets.
public static bool Filter(string game)
{
return game.Contains(" ");
}
public static string ProcessItem(string game)
{
return game;
}
}

You can test this iteration of your string-processing logic by calling this method within the top-level statements of the Program.cs file, as follows:

VeryComplexQueryExpression.QueryStringsWithRawDelegates();

If you were to now run the application to test each possible approach, it should not be too surprising that the output is identical, regardless of the path taken. Keep the following points in mind regarding how LINQ query expressions are represented under the covers:
• Query expressions are created using various C# query operators.
• Query operators are simply shorthand notations for invoking extension methods defined by the System.Linq.Enumerable type.
• Many methods of Enumerable require delegates (Func<> in particular) as parameters.
• Any method requiring a delegate parameter can instead be passed a lambda expression.
• Lambda expressions are simply anonymous methods in disguise (which greatly improve readability).
• Anonymous methods are shorthand notations for allocating a raw delegate and manually building a delegate target method.
Whew! That might have been a bit deeper under the hood than you wanted to go, but I hope this discussion has helped you understand what the user-friendly C# query operators are doing behind the scenes.

Summary
LINQ is a set of related technologies attempting to provide a single, symmetrical manner to interact with diverse forms of data. As explained over the course of this chapter, LINQ can interact with any type implementing the IEnumerable interface, including simple arrays as well as generic and nongeneric collections of data.

As you have seen, working with LINQ technologies is accomplished using several C# language features. For example, given that LINQ query expressions can return any number of result sets, it is common to make use of the var keyword to represent the underlying data type. Lambda expressions, object initialization syntax, and anonymous types can all be used to build functional and compact LINQ queries.
More importantly, you have seen how the C# LINQ query operators are simply shorthand notations for making calls on static members of the System.Linq.Enumerable type. As shown, most members of Enumerable operate on Func delegate types, which can take literal method addresses, anonymous methods, or lambda expressions as input to evaluate the query.

发表评论