Tải bản đầy đủ (.doc) (26 trang)

An introduction to LINQ

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (305.69 KB, 26 trang )

CHAPTER 1 4

An Introduction to LINQ

T

he previous chapter introduced you to numerous C# 2008 programming constructs. As you have 
seen, features such as implicitly typed local variables, anonymous types, object initialization syntax, 
and lambda expressions (examined in Chapter 11) allow us to build very functional C# code. Recall 
that while many of these features can be used directly as is, their benefits are much more apparent 
when used within the context of the Language Integrated Query (LINQ) technology set.
This chapter will introduce you to the LINQ 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 vari­ 
ous collection types (both generic and nongeneric) and understand the assemblies and types that 
enable LINQ.

■Note Chapter 24 will examine additional LINQ-centric APIs that allow you to interact with relational databases
and XML documents.

Understanding the Role of LINQ
As software developers, it is hard to deny that the vast majority of our programming time is spent 
obtaining and manipulating data. When speaking of “data,” it is very easy to immediately envision
information contained within relational databases. However, another popular location in which 
data exists is within XML documents (*.config files, locally persisted DataSets,  in­memory data 
returned from XML web services, etc.).
Data can be found in numerous places beyond these two common homes for information. For 
instance, say you have a generic List<T> 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, etc.). Or perhaps you are making use of the 


reflection APIs and need to obtain only metadata descriptions for each class deriving from a partic­ 
ular parent class within an array of Types. Indeed, data is everywhere.
Prior to .NET 3.5, interacting with a particular flavor of data required programmers to make use 
of diverse APIs. Consider, for example, Table 14­1, which illustrates several common APIs used to 
access various types of data.

447


192

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

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

The Data We Want

How to Obtain It

Relational data
System.Data.dll,     
S y s t e m . D a t a . S q l C l i e n t . d l l ,   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, when pro­ 
gramming with .NET 3.5/C# 2008, 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 very 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 manner in which program­ 
mers can obtain and manipulate “data” in the broad sense of the term. Using LINQ, we are able to
create directly within the C# programming language entities called query expressions. These query 
expressions are based on numerous query operators that have been intentionally designed to look 
and feel very 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. Specifically, LINQ allows query 
expressions to manipulate any object that implements the IEnumerable<T> interface (directly or 
indirectly via extension methods), relational databases, DataSets,  or XML documents in a consistent 
manner.

■Note Strictly speaking, “LINQ” is the term used to describe this overall approach to data access. LINQ to
Objects is LINQ over objects implementing IEnumerable<T>, LINQ to SQL is LINQ over relational data, LINQ to
DataSet is a superset of LINQ to SQL, and LINQ to XML is LINQ over XML documents. In the future, you are sure
to find other APIs that have been injected with LINQ functionality (in fact, there are already other LINQ-centric
projects under development at Microsoft).

LINQ Expressions Are Strongly Typed and Extendable
It is also very important to point out that a LINQ query expression (unlike a traditional SQL state­ 
ment) is strongly typed. Therefore, the C# compiler will keep us honest and make sure that these 
expressions are syntactically well formed. On a related note, query expressions have metadata rep­
resentation within the assembly that makes use of them. Tools such as Visual Studio 2008 can use 
this metadata for useful features such as IntelliSense, autocompletion, and so forth.
Also, before we dig into the details of LINQ, one final point is that LINQ is designed to be an 
extendable technology. While this initial release of LINQ is targeted for relational databases/ 
DataSets,  XML documents, and objects implementing IEnumerable<T>, third parties can incorporate 
new query operators (or redefine existing operators) using extension methods (see Chapter 13) to 

account for addition forms of data.


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

■Note Before you continue reading over this chapter, I wholeheartedly recommend that you first feel comfortable
with the material presented in Chapter 13 (which covered C# 2008 specific constructs). As you will see, LINQ
pro- gramming makes use of several of the new C# features to simplify coding tasks.

The Core LINQ Assemblies
As mentioned in Chapter 2, the New Project dialog of Visual Studio 2008 now has the option of 
selecting which version of the .NET platform you wish to compile against, using the drop­down list 
box mounted on the upper­right corner. When you opt to compile against the .NET Framework 3.5,
each of the project templates will automatically reference the key LINQ assemblies. For example, if 
you were to create a new .NET 3.5 Console Application, you will find the assemblies shown in
Figure 14­1 visible within the Solution Explorer.

Figure 14-1. .NET 3.5 project types automatically reference key LINQ assemblies.
Table 14­2 documents the role of the core LINQ­specific assemblies.
Table 14-2. Core LINQ­centric Assemblies

Assembly

Meaning in Life

System.Core.dll

Defines the types that represent the core LINQ API. This
is the one assembly you must have access to.


System.Data.Linq.dll

Provides functionality for using LINQ with relational 
databases (LINQ to SQL).

System.Data.DataSetExtensions.dll

Defines a handful of types to integrate ADO.NET types 
into the LINQ programming paradigm (LINQ to DataSet).
Provides functionality for using LINQ with XML
document data (LINQ to XML).

System.Xml.Linq.dll

When you wish to do any sort of LINQ programming, you will at the very least need to import
the System.Linq namespace (defined within System.Core.dll),      which is typically accounted for by
new Visual Studio 2008 project files; for example, here is the starting code for a new .NET 3.5 Con­
sole Application project:

193


194

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

using
using
using
using


System;
System.Collections.Generic;
System.Linq;
System.Text;

namespace MyConsoleApp
{
class Program
{
static void Main(string[] args)
{
}
}
}

A First Look at LINQ Query Expressions
To begin examining the LINQ programming model, let’s build simple query expressions to 
manipulate data contained within various arrays. Create a .NET 3.5 Console Application 
named LinqOverArray, and define a static helper method within the Program class named 
QueryOverStrings().     In this method, create a string array containing six or so items of your 
liking (here, I listed out a batch of video games I am currently attempting to finish).
static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "BioShock",
"Half Life 2: Episode 1", "The Darkness",
"Daxter", "System Shock 2"};
Console.ReadLine();
}

Now, update Main() to invoke QueryOverStrings():
static void Main(string[] args)
{
Console.WriteLine("*****
Fun with LINQ *****\n");
QueryOverStrings();
Console.ReadLine();
}
When you have any array of data, it is very common to extract a subset of items based on a
given requirement. Maybe you want to obtain only the items with names that contain a number 
(e.g., System Shock 2 and Half Life 2: Episode 1), have more than some number of characters, or 
don’t have embedded spaces (e.g., Morrowind). 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 we wish to obtain a subset from the array that contains items 
with names consisting of more than six characters, we could build the following query expression:
static void QueryOverStrings()
{
// Assume we have an array of strings.
string[] currentVideoGames = {"Morrowind", "BioShock",
"Half Life 2: Episode 1", "The Darkness",
"Daxter", "System Shock 2"};


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

// Build a query expression to represent the items in the array
// that have more than 6 letters.
IEnumerable<string> subset = from g in currentVideoGames
where g.Length > 6 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. We will dig into the formalities of query expression syntax in just 
a bit, but even now you should be able to parse this statement as “Give me the items inside of 
currentVideoGames that have more than six characters, ordered alphabetically.” Here, each item that
matches the search criteria has been given the name “g” (as in “game”); however, any valid C# vari­ 
able name would do:
IEnumerable<string> subset = from game in currentVideoGames
where game.Length > 6 orderby game select g a m e ;
Notice that the “result set” variable, subset, is represented by an object that implements the 
generic version of IEnumerable<T>, where T is of type System.String (after all, we are querying an 
array of strings). Once we obtain the result set, we then simply print out each item using a standard 
foreach construct.
Before we see the results of our query, assume the Program class 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)
{
Console.WriteLine("*****
Info about your query *****");
Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name);
Console.WriteLine("resultSet location: {0}", resultSet.GetType().Assembly);
}
Assuming you have called this method within QueryOverStrings() directly after printing

out the obtained subset, if you run the application, you will see the subset is really an instance of 
the generic OrderedEnumerable<TElement, TKey> type (represented in terms of CIL code as 
OrderedEnumerable`2),   which is an internal abstract type residing in the System.Core.dll assembly 
(see Figure 14­2).

Figure 14-2. The result of our LINQ query

195


196

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

■Note Many of the types that represent a LINQ result are hidden by the Visual Studio 2008 object browser. Make
use of ildasm.exe or reflector.exe to see these internal, hidden types.

LINQ and Implicitly  Typed Local Variables
While the current sample program makes it relatively easy to determine that the result set is enu­ 
merable as a string collection, I would guess that it is not clear that subset is really of type 
OrderedEnumerable<TElement, TKey>. Given the fact 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 
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 class (which I assume you will invoke from within the Main() method):
static void QueryOverInts()
{
int[] numbers = {10, 20, 30, 40, 1, 2, 3, 8};

// Only print items less than 10.
IEnumerable<int> 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 obtained (under the covers) by calling the System.Linq.
Enumerable.Where<T> method, passing in a compiler­generated  anonymous method as the second
parameter. Here is the crux of the internal definition of the subset variable generated by the com­
piler (assume the anonymous method has been named 9 CachedAnonymousMethodDelegate8):
// The following LINQ query expression:
//
// IEnumerable<int> subset = from i in numbers where i < 10 select i;
//
// Is transformed into a call to the Enumerable.Where<int>() method:
//
IEnumerable<int> subset = Enumerable.Where<int>(numbers,
Program.<>9 CachedAnonymousMethodDelegate8);

■Note I would recommend that you load LINQ-based applications into a decompiler such as ildasm.exe or
reflector.exe. These sorts of tools can greatly strengthen your understanding of LINQ internals.

Without diving too deeply into the use of Enumerable.Where<T> at this point, do note that in 
Figure 14­3, the underlying type for each query expression is indeed unique, based on the format of 
our LINQ query expression.


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q


Figure 14-3. LINQ query expressions can return numerous result sets.
Given the fact that the exact underlying type of a LINQ query is certainly not obvious, the 
current example has represented the query results as local IEnumerable<T> variable. Given that
IEnumerable<T> 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;
While this is syntactically correct, 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);
}
Recall that the var keyword should not be confused with the legacy COM Variant or loosely
typed variable declaration found in many scripting languages. The underlying type is determined 
by the compiler based on the result of the initial assignment. After that point, it is a compiler error 
to attempt to change the “type of type.” Furthermore, given the fact that in many cases the under­ 
lying type is the result of a dynamically generated anonymous type, it is commonplace to use 
implicit typing whenever you wish to capture a LINQ result set.

LINQ and Extension Methods
Recall from the previous chapter that extension methods make it possible to add new members to a 

previously compiled type within the scope of a given project. Although the current example does 
not have you author any extension methods directly, you are in fact using them seamlessly in the

197


198

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

background. LINQ query expressions can be used to iterate over data containers that implement
the generic IEnumerable<T> interface. However, the .NET System.Array class type (used to represent
our array of strings and array of integers) does not implement this behavior:
// The System.Array type does not seem to implement the correct
// infrastructure for query expressions!
public abstract class Array : ICloneable, IList, ICollection, IEnumerable
{
...
}
While System.Array does not directly implement the IEnumerable<T> 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 type defined a good number of generic extension 
methods (such as Aggregate<T>(),   F i r s t < T > ( ) ,   Max<T>(), etc.), which System.Array (and other 
types) acquire 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 (see Figure 14­4).

Figure 14-4. The System.Array type has been extended with members of System.Linq.Enumerable.

The Role of Differed Execution

Another important point regarding LINQ query expressions is that they are not actually evaluated 
until you iterate over their contents. Formally speaking, this is termed differed execution. The bene­
fit of this approach is that you are able to 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 };


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

// 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;
// Evaluate again.
foreach (var j in subset)
Console.WriteLine("{0}
< 10", j);
ReflectOverQueryResults(subset);
}
If you were to execute the program yet again, you will find the output shown in Figure 14­5.

Figure 14-5. LINQ expressions are executed when evaluated.

One very useful aspect of Visual Studio 2008 is that if you set a breakpoint before the evaluation
of a LINQ query, you are able to view the contents during a debugging session. Simply locate your 
mouse cursor above the LINQ result set variable (subset in Figure 14­6). When you do, you will be 
given the option of evaluating the query at that time by expanding the Results View option.

Figure 14-6. Debugging LINQ expressions

199


200

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

The Role of Immediate  Execution
When you wish to evaluate a LINQ expression from outside the confines of foreach logic, you are 
able to call any number of extension methods defined by the Enumerable type to do so. Enumerable
defines a number of extension methods such as ToArray<T>(),  ToDictionary<TSource,TKey>(),     and 
ToList<T>(),   which allow you to capture a LINQ query result set in a strongly typed container. Once 
you have done so, the container is no longer “connected” to the LINQ expression, and may be inde­
pendently manipulated:
static void ImmediateExecution()
{
int[] numbers = { 10, 20, 30, 40, 1, 2, 3, 8 };
// Get data RIGHT NOW as int[].
int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray<int>();
// Get data RIGHT NOW as List<int>.
List<int> subsetAsListOfInts
=

(from i in numbers where i < 10 select i).ToList<int>();
}
Notice that the entire LINQ expression is wrapped within parentheses to cast it into the correct
underlying type (whatever that may be) in order 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 item, you are not required to specify the type parameter. Thus, we could also
call ToArray<T>() (or ToList<T>() for that matter) as follows:
int[] subsetAsIntArray =
(from i in numbers where i < 10 select i).ToArray();

■Source Code The LinqOverArray project can be found under the Chapter 14 subdirectory.

LINQ and Generic Collections
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<T> type. 
Create a new .NET 3.5 Console Application project named LinqOverCustomObjects, and define a 
basic Car type that maintains a current speed, color, make, and pet name (public fields are used to
easily set the string fields to empty text. Feel free to make use of automatic properties and class 
constructors if you wish):
class Car
{
public string PetName = string.Empty;
public string Color = string.Empty;
public int Speed;
public string Make = string.Empty;
}
Now, within your Main() method, define a local List<T> variable of type Car, and make use of
the new object initialization syntax (see Chapter 13) to fill the list with a handful of new Car objects:



CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

static void Main(string[] args)
{
Console.WriteLine("*****
More fun with LINQ Expressions

*****\n");

// Make a List<> of Car objects
// using object init syntax.
List<Car> myCars = new List<Car>() {
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"}
};
}

Applying a LINQ Expression
Our goal is to build a query expression to select only the items within the myCars list, where the 
speed is greater than 55. Once we get the subset, we will print out the name of each Car object. 
Assume you have the following helper method (taking a List<Car> parameter), which is called from 
within Main():
static void GetFastCars(List<Car>
myCars)
{
// Create a query expression.

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 our query expression is only grabbing items from the List<T> where the Speed
property is greater than 55. If we run the application, we will find that “Henry” and “Daisy” are the
only two items that match the search criteria.
If we want to build a more complex query, we might wish to only find the BMWs that have a
Speed value above 90. To do so, simply build a compound Boolean statement using the C# &&
operator:
// Create a query expression.
var fastCars = from c in myCars where
c.Speed > 90 && c.Make == "BMW" select c;
In this case, the only pet name printed out is “Henry”.

■Source Code The LinqOverCustomObjects project can be found under the Chapter 14 subdirectory.

LINQ and Nongeneric Collections
Recall that the query operators of LINQ are designed to work with any type implementing
IEnumerable<T> (either directly or via extension methods). Given that System.Array has been

201


202

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ


provided with such necessary infrastructure, it may surprise you that the legacy (nongeneric) con­
tainers within System.Collections have not. Thankfully, it is still possible to iterate over data 
contained within nongeneric collections using the generic Enumerable.OfType<T>() method.
The OfType<T>() method is one of the few members of Enumerable that does not extend generic 
types. When calling this member off a nongeneric container implementing the IEnumerable
interface (such as the ArrayList),    simply specify the type of item within the container to extract 
a compatible IEnumerable<T> object. Assume we have a new Console Application named 
LinqOverArrayList that defines the following Main() method (note that we are making use of the 
previously defined Car type and be sure to import the System.Collections namespace).
static void Main(string[] args)
{
Console.WriteLine("*****
LINQ over ArrayList

*****\n");

// 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<T>-compatible type.
IEnumerable<Car> myCarsEnum = myCars.OfType<Car>();
// Create a query expression.
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);
}
}

Filtering Data Using OfType<T>()
As you know, nongeneric types are capable of containing any combination of items, as the mem­
bers 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 we want to obtain a subset that contains only numerical data, we can do so using OfType<T>(), 
since it filters out each element whose type is different from the given type during the iterations:
// Extract the ints from the ArrayList.
ArrayList myStuff = new ArrayList();
myStuff.AddRange(new object[] { 10, 400, 8, false, new Car(), "string data" });
IEnumerable<int> myInts = myStuff.OfType<int>();
// Prints out 10, 400, and 8.
foreach (int i in myInts)
{
Console.WriteLine("Int
value: {0}", i);
}


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

■Source Code The LinqOverArrayList project can be found under the Chapter 14 subdirectory.
Now that you have seen how to use LINQ to manipulate data contained within various arrays
and collections, let’s dig in a bit deeper to see what is happening behind the scenes.


The Internal Representation of LINQ Query
Operators
So at this point you have been briefly introduced to the process of building query expressions using 
various C# query operators (such as from, in, where, orderby,   and select). When compiled, the C# 
compiler actually translates these tokens into calls on various methods of the System.Linq.
Enumerable type (and possibly other types, based on your LINQ query).
As it turns out, a great many of the methods of Enumerable have been prototyped to take dele­
gates as arguments. In particular, many methods require a generic delegate of type Func<>, defined
within the System namespace of System.Core.dll.      For example, consider the following members of
Enumerable that extend the IEnumerable<T> interface:
// Overloaded versions of the Enumerable.Where<T>() method.
// Note the second parameter is of type System.Func<>.
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source,
System.Func<TSource,int,bool> predicate)
public static IEnumerable<TSource> Where<TSource>(this
System.Func<TSource,bool> predicate)

IEnumerable<TSource> source,

This delegate (as the name implies) represents a pattern for a given function with a set of argu­ 
ments and a return value. If you were to examine this type using the Visual Studio 2008 object 
browser, you’ll notice that the Func<> delegate can take between zero and four input arguments 
(here typed T0, T1, T2, and T3 and named arg0,  arg1,  arg2,  and arg3),  and a return type denoted by 
TResult:
// The various formats
public delegate TResult
T0 arg0, T1 arg1, T2
public delegate TResult
public delegate TResult
public delegate TResult

public delegate TResult

of the Func<> delegate.
Func<T0,T1,T2,T3,TResult>(
arg2, T3 arg3)
Func<T0,T1,T2,TResult>(T0 arg0, T1 arg1, T2 arg2)
Func<T0,T1,TResult>(T0 arg0, T1 arg1)
Func<T0,TResult>(T0 arg0)
Func<TResult>()

Given that many members of System.Linq.Enumerable demand a delegate as input, when 
invoking them, we can either manually create a new delegate type and author the necessary target 
methods, make use of a C# anonymous method, or define a proper lambda expression. Regardless 
of which approach you take, the end result is identical.
While it is true that making use of 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 named LinqUsingEnumerable. The Program class will
define a series of static helper methods (each of which is called within the Main() method) to

203


204

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

illustrate the various manners in which we 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 seen in the previous LinqOverArray example:
static void QueryStringWithOperators()
{
Console.WriteLine("***** Using Query Operators *****");
string[] currentVideoGames = {"Morrowind", "BioShock",
"Half Life 2: Episode 1", "The Darkness",
"Daxter", "System Shock 2"};
// Build a query expression using query operators.
var subset = from g in currentVideoGames
where g.Length > 6 orderby g select g;
// Print out the results.
foreach (var s in subset)
Console.WriteLine("Item:
{0}", s);
}
The obvious benefit of using C# query operators to build query expressions is the fact that the
Func<> delegates and calls on the Enumerable type are out of sight and out of mind, 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, orderby,   etc.) is the most common and most straightforward 
approach.

Building  Query Expressions Using the Enumerable Type
and Lambdas
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", "BioShock",
"Half Life 2: Episode 1", "The Darkness",
"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.Length > 6)
.OrderBy(game => game).Select(game => game);
// Print out the results.
foreach (var game in subset)
Console.WriteLine("Item:
{0}", game);
Console.WriteLine();
}
Here, we are calling the generic Where() method off the string array object, granted to the Array
type as an extension method defined by Enumerable.  The Enumerable.Where<T>() method makes use 
of the System.Func<T0, TResult> delegate type. The first type parameter of this delegate represents


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

the IEnumerable<T>­compatible data to process (an array of strings in this case), while the second
type parameter represents the method that will process said data.
Given that we have opted for a lambda expression (rather than directly creating an instance
of Func<T> or crafting an anonymous method), we are specifying that the “game” parameter is 
processed by the statement game.Length > 6, which results in a Boolean return type.

The return value of the Where<T>() method has implicitly typed, but under the covers we are 
operating on an OrderedEnumerable type. From this resulting object, we call the generic OrderByK>() method, which also requires a Func<T, K> delegate parameter. Finally, from the result of the 
specified lambda expression, we select each element, using once again a Func<T, K> under the 
covers.
It is also worth remembering that extension methods are unique in that they can be called as 
instance­level members upon the type they are extending (System.Array in this case) or as static 
members using the type they were defined within. Given this, we could also author our query 
expression as follows:
var subset = Enumerable.Where(currentVideoGames,
.OrderBy(game => game).Select(game => game);

game => game.Length > 6)

As you may agree, building a LINQ query expression using the methods of the Enumerable type 
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# 2008 lambda expressions are simply shorthand notations for working with 
anonymous methods, consider the third query expression created within the 
QueryStringsWithAnonymousMethods() helper function:
static void QueryStringsWithAnonymousMethods()
{
Console.WriteLine("*****
Using Anonymous Methods *****");
string[] currentVideoGames = {"Morrowind", "BioShock",
"Half Life 2: Episode 1", "The Darkness",

"Daxter", "System Shock 2"};
// Build the necessary Func<> delegates using anonymous methods.
Func<string, bool> searchFilter =
delegate(string game) { return game.Length > 6; };
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 we are manually creating
the Func<> delegates used by the Where(),  OrderBy(),   and Select() methods of the Enumerable type.

205


206

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

On the plus side, the anonymous method syntax does keep all the 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 we want to build a query expression using the really verbose approach, we could avoid the 
use of lambdas/anonymous method syntax and directly create delegate targets for each Func<>
type. Here is the final iteration of our query expression, modeled within a new class type named 
VeryComplexQueryExpression:
class VeryComplexQueryExpression
{
public static void QueryStringsWithRawDelegates()
{
Console.WriteLine("*****
Using Raw Delegates *****");
string[] currentVideoGames = {"Morrowind", "BioShock",
"Half Life 2: Episode 1", "The Darkness",
"Daxter", "System Shock 2"};
// Build the necessary Func<> delegates using anonymous methods.
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 s) {return s.Length > 6;}
public static string ProcessItem(string s) { return s; }

}
We can test this iteration of our string processing logic by calling this method within Main()
method of the Program class as follows:
VeryComplexQueryExpression.QueryStringsWithRawDelegates();
If you were to now run the application to test each possible approach, it should not be too sur­ 
prising 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.


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

•   Under C# 2008, 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 wish to have gone, but I 
hope this discussion has helped you understand what the user­friendly C# query operators are 
actually doing behind the scenes. Let’s now turn our attention to the operators themselves.

■Source Code The LinqOverArrayUsingEnumerable project can be found under the Chapter 14 subdirectory.

Investigating the C# LINQ Query Operators
C# defines a good number of query operators out of the box. Table 14­3 documents some of the
more commonly used query operators.


■Note The .NET Framework 3.5 SDK documentation provides full details regarding each of the C# LINQ operators. Look up the topic “LINQ General Programming Guide” for more information.

Table 14-3. Various 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.
Used to select a sequence from the container.

select
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.
Yields a subset with data grouped by a specified value.

group,  by

In addition to the partial list of operators shown in Table 14­3, the Enumerable type 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 var­ 
ious manners (Reverse<>(),   ToArray<>(),  ToList<>(),   etc.). Some are used to extract singletons
from a result set, others perform various set operations (Distinct<>(), Union<>(), 
I n t e r s e c t < > ( ) ,   etc.), and still others aggregate results (Count<>(),  Sum<>(), Min<>(), 
Max<>(), etc.).

207


208

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

Obtaining Counts Using Enumerable
Using these query operators (and auxiliary members of the System.Linq.Enumerable type), you are 
able to build very expressive query expressions in a strongly typed manner. To invoke the 
Enumerable extension methods, you typically wrap the LINQ expression within parentheses to cast 
the result to an IEnumerable<T>­compatible object to invoke the Enumerable extension method.
You have already done so during our examination of immediate execution; however, here is
another example that allows you to discover the number of items returned by a LINQ query:
static void GetCount()
{
string[] currentVideoGames = {"Morrowind", "BioShock",

"Half Life 2: Episode 1", "The Darkness",
"Daxter", "System Shock 2"};
// Get count from the query.
int numb = (from g in currentVideoGames
where g.Length > 6
orderby g
select g).Count<string>();
// numb is the value 5.
Console.WriteLine("{0}
items honor the LINQ query.", numb);
}

Building  a New Test Project
To begin digging into more intricate LINQ queries, create a new Console Application named 
FunWithLinqExpressions. Next, define a trivial Car type, this time sporting a custom ToString()
implementation to quickly view the object’s state:
class Car
{
public string PetName = string.Empty;
public string Color = string.Empty;
public int Speed;
public string Make = string.Empty;
public override string ToString()
{
return string.Format("Make={0},
Color={1}, Speed={2}, PetName={3}",
Make, Color, Speed, PetName);
}
}
Now populate an array with the following Car objects within your Main() method:

static void Main(string[] args)
{
Console.WriteLine("*****
Fun with Query Expressions
// This array will
Car[] myCars = new
new Car{ PetName
new Car{ PetName
new Car{ PetName
new Car{ PetName

*****\n");

be the basis of our testing...
[] {
= "Henry", Color = "Silver", Speed = 100, Make = "BMW"},
= "Daisy", Color = "Tan", Speed = 90, Make = "BMW"},
= "Mary", Color = "Black", Speed = 55, Make = "VW"},
= "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"},


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

new
new
new
new
new

Car{

Car{
Car{
Car{
Car{

PetName
PetName
PetName
PetName
PetName

=
=
=
=
=

"Hank", Color = "Tan", Speed = 0, Make = "Ford"},
"Sven", Color = "White", Speed = 90, Make = "Ford"},
"Mary", Color = "Black", Speed = 55, Make = "VW"},
"Zippy", Color = "Yellow", Speed = 55, Make = "VW"},
"Melvin", Color = "White", Speed = 43, Make = "Ford"}

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

Basic Selection Syntax
Because LINQ query expressions are 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:
var result = from item in container

select i t e m ;

In this case, our query expression is doing nothing more than selecting every item in the con­ 
tainer (similar to a Select * SQL statement). Consider the following:
static void BasicSelection(Car[] myCars)
{
// Get everything.
Console.WriteLine("All
cars:");
var allCars = from c in myCars select c;
foreach (var c in allCars)
{
Console.WriteLine(c.ToString());
}
}
Again, this query expression is not entirely useful, given that our subset is identical to that of
the data in the incoming parameter. If we wish, we could use this incoming parameter to extract
only the PetName values of each car using the following selection syntax:
// Now get only the names of the cars.
Console.WriteLine("Only
PetNames:");
var names = from c in myCars select c.PetName;
foreach (var n in names)
{
Console.WriteLine("Name:
}


{0}", n);

In this case, names is really an internal type that implements IEnumerable<string>,     given that
we are selecting only the values of the PetName property for each Car object. Again, using implicit
typing via the var keyword, our coding task is simplified.
Now consider the following task. What if you’d like to obtain and display the makes of each
vehicle? If you author the following query expression:
var makes = from c in myCars select c.Make;
you will end up with a number of redundant listings, as you will find BMW, Ford, and VW 
accounted for multiple times. You can use the Enumerable.Distinct<T>()
method to eliminate
such duplication:

209


210

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

var makes = (from c in myCars select c.Make).Distinct<string>();
When calling any extension method defined by Enumerable, you can do so either at the time
you build the query expression (as shown in the previous example) or via an extension method on a 
compatible underlying array type. Thus, the following code yields identical output:
var makes = from c in myCars select c.Make;
Console.WriteLine("Distinct
makes:");
foreach (var m in makes.Distinct<string>())
{

Console.WriteLine("Make:
{0}", m);
}
Figure 14­7 shows the result of calling BasicSelections().

Figure 14-7. Selecting basic data from the Car[] parameter

Obtaining Subsets of Data
To obtain a specific subset from a container, you can make use of the where operator. When doing
so, the general template now becomes as follows:
var result = from item in container

where Boolean expression

select item;

Notice that the where operator expects an expression that resolves to a Boolean. For example,
to extract from the Car[] parameter only the items that have “BMW” as the value assigned to the 
Make field, you could author the following code within a new method named GetSubsets():
static void GetSubsets(Car[]
myCars)
{
// Now get only the BMWs.
var onlyBMWs = from c in myCars where c.Make == "BMW" select c;


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

foreach (Car c in onlyBMWs)
{

Console.WriteLine(c.ToString());
}
}
As seen 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, consider the following
query that only extracts out 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;
foreach (Car c in onlyFastBMWs)
{
Console.WriteLine("{0}
is going {1} MPH", c.PetName, c.Speed);
}

Projecting New Data Types
It is also possible to project new forms of data from an existing data source. Let’s assume that you 
wish to take the incoming Car[] parameter and obtain a result set that accounts only for the make 
and color of each vehicle. To do so, you can define a select statement that dynamically yields new 
types via C# 2008 anonymous types. Recall from Chapter 13 that the compiler defines a read­only 
property and a read­only backing field for each specified name, and also is kind enough to override 
ToString(), GetHashCode(),  and Equals():
var makesColors = from c in myCars select new {c.Make, c . C o l o r } ;
foreach (var o in makesColors)
{
// Could also use Make and Color properties directly.
Console.WriteLine(o.ToString());
}
Figure 14­8 shows the output of each of these new queries.


Figure 14-8. Enumerating over subsets

211


212

CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

Reversing Result Sets
You can reverse the items within a result set quite simply using the generic Reverse<T>() method of
the Enumerable type. For example, the following method selects all items from the incoming Car[]
parameter in reverse:
static void ReversedSelection(Car[]
myCars)
{
// Get everything in reverse.
Console.WriteLine("All cars in reverse:");
var subset = (from c in myCars select c).Reverse<Car>();
foreach (Car c in subset)
{
Console.WriteLine("{0}
is going {1} MPH", c.PetName, c.Speed);
}
}
Here, we called the Reverse<T>() method at the time we constructed our query. Again, as an
alternative, we could invoke this method on the myCars array as follows:
static void ReversedSelection(Car[]
myCars)

{
// Get everything in reverse.
Console.WriteLine("All
cars in reverse:");
var subset = from c in myCars select c;
foreach (Car c in subset.Reverse<Car>())
{
Console.WriteLine(c.ToString());
}
}

Sorting Expressions
As you have seen over 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 wish to view the results in a descending order, simply include the descending
operator. Ponder the following method:
static void OrderedResults(Car[]
myCars)
{
// Order all the cars by PetName.
var subset = from c in myCars orderby c.PetName select c;
Console.WriteLine("Ordered
by PetName:");
foreach (Car c in subset)
{
Console.WriteLine(c.ToString());
}
// Now find the cars that are going less than 55 mph,
// and order by descending PetName

subset = from c in myCars
where c.Speed > 55 orderby c.PetName descending select c;
Console.WriteLine("\nCars
going faster than 55, ordered by PetName:");
foreach (Car c in subset)
{


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

Console.WriteLine(c.ToString());
}
}
Although ascending order is the default, you are able to make your intentions very clear by
making use of the ascending operator:
var subset = from c in myCars
orderby c.PetName ascending select c;
Given these examples, you can now understand the format of a basic sorting query expression 
as follows:
var result = from item in container orderby value
ascending/descending
select item;

Finding Differences
The last LINQ query we will examine for the time being involves obtaining a result set that deter­ 
mines the differences between two IEnumerable<T> compatible containers. Consider the following 
method, which makes use of the Enumerable.Except() method to yield (in this example) a Yugo:
static void GetDiff()
{
List<string> myCars = new List<String>

{ "Yugo", "Aztec", "BMW"};
List<string> yourCars = new List<String>
{ "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.
}
These examples should give you enough knowledge to feel comfortable with the process of
building LINQ query expressions. Chapter 24 will explore the related topics of LINQ to ADO (which 
is a catch­all term describing LINQ to SQL and LINQ to DataSet) and LINQ to XML. However, before
wrapping the current chapter, let’s examine the topic LINQ queries as method return values.

■Source Code The FunWithLinqExpressions project can be found under the Chapter 14 subdirectory.

LINQ Queries: An Island unto Themselves?
You may have noticed that each of the LINQ queries seen over the course of this chapter were all 
defined within the scope of a local method. Moreover, to simplify our programming, the variable 
used to hold the result set was stored in an implicitly typed local variable (in fact, in the case of pro­ 
jections, this is mandatory). Recall from Chapter 13 that implicitly typed local variables cannot be 
used to define parameters, return values, or fields of a class type.

213


214


CHAPTE R 14 ■ AN IN TRODUCTI O N TO L INQ

Given this point, you may 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, a List<T> of Cars, or whatnot), you could abandon the use of the var keyword and 
using a proper IEnumerable<T> or IEnumerable type (again, as IEnumerable<T> extends IEnumerable). 
Consider the following example for a new .NET 3.5 Console Application named LinqRetValues:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*****
LINQ Transformations *****\n");
IEnumerable<string> subset = GetStringSubset();
foreach (string item in subset)
{
Console.WriteLine(item);
}
Console.ReadLine();
}
static IEnumerable<string> GetStringSubset()
{
string[] currentVideoGames = {"Morrowind", "BioShock",
"Half Life 2: Episode 1", "The Darkness",
"Daxter", "System Shock 2"};
// Note subset is an IEnumerable<string> compatible object.
IEnumerable<string> subset = from g in currentVideoGames
where g.Length > 6
orderby g
select g;

return subset;
}
}
This example works as expected, only because the return value of the 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<string> (and if the implicitly typed local variable is in fact compatible with 
the specified return type).
However, 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; therefore, the following code method would not 
compile:
// Error! Can't return a var data type!
static var GetProjectedSubset()
{
Car[] myCars = new Car[] {
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"}
};


CHAPTE R 14 ■ AN IN TRODUCTI O N TO LIN Q

var makesColors = from c in myCars select new { c.Make, c.Color
return makesColors; // Nope!

};


}
Given that return values cannot be implicitly typed, how can we return the makesColors object
to an external caller?

Transforming  Query Results to Array Types
When you wish to return projected data to a caller, one approach is to transform the query result
into a standard CLR Array object using the ToArray<T>() extension method. Thus, if we were to 
update our query expression as follows:
// Return value is now an Array.
static Array GetProjectedSubset()
{
Car[] myCars = new Car[]{
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"}
};
var makesColors = from c in myCars select new { c.Make, c.Color

};

// Map set of anonymous objects to an Array object.
// Here were are relying on type inference of the generic
// type parameter, as we don't know the type of type!
return makesColors.ToArray();
}
we could invoke and process the data from Main() as follows:
Array objs = GetProjectedSubset();

foreach (object o in objs)
{
Console.WriteLine(o);
// Calls ToString()
}

on each anonymous object.

Note that we have to use a literal System.Array object and cannot make use of the C# array
declaration syntax, given that we don’t know the underlying type of type! Also note that we are not
specifying the type parameter to the generic ToArray<T>() method, as we (once again) don’t know
the underlying data type until compile time (which is too late for our purposes).
The obvious problem is that we 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 which is the 
result of a projection operation, transforming the data into an Array type (or another suitable con­ 
tainer via other members of the Enumerable type) is mandatory.

■Source Code The LinqRetValues project can be found under the Chapter 14 subdirectory.

215


Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Tải bản đầy đủ ngay
×