NOTE
LINQ is a deeply
integrated part of .NET and the VB language. However, it isn't an
ASP.NET-specific feature, and it can be used equally well in any type of
.NET application, from command-line tools to rich Windows clients.
2. LINQ Basics
The easiest way to approach
LINQ is to consider how it works with in-memory collections. This is
LINQ to Objects—the simplest form of LINQ.
First, imagine you have some sort of data class, like the Employee class shown here:
Public Class Employee
Public Property EmployeeID() As Integer
Public Property FirstName() As String
Public Property LastName() As String
Public Property TitleOfCourtesy() As String
Public Sub New(ByVal employeeID As Integer, ByVal firstName As String, _
ByVal lastName As String, ByVal titleOfCourtesy As String)
Me.EmployeeID = employeeID
Me.FirstName = firstName
Me.LastName = lastName
Me.TitleOfCourtesy = titleOfCourtesy
End Sub
End Class
This exceedingly simple
class includes just four properties and a basic constructor. You can
easily create a collection that consists of Employee objects, like the
strongly typed List shown here:
' Create the collection.
Dim employees As New List(Of Employee)()
' Fill the collection.
employees.Add(New Employee(1, "Nancy", "Davolio", "Ms."))
employees.Add(New Employee(2, "Andrew", "Fuller", "Dr."))
employees.Add(New Employee(3, "Janet", "Leverling", "Ms."))
...
In this example, the data for
each Employee object is hard-coded, but you could just as easily read it
from an XML document, a database, or some other location. The key point
is that when you're finished, you're left with some sort of collection
that contains one or more objects. You can then use LINQ to Objects to
get at the data in your collection.
Before you use a LINQ expression, it's worth considering the traditional approach for searching a collection.
For example, imagine you want to get a list of all employees who have a last name that starts with the letter D.
The traditional approach is to use code to loop through the full
collection of employees and add each matching employee to a second
collection, as shown here:
' Create the source collection.
Dim employees As New List(Of Employee)()
' (Code for filling the collection omitted to save space.)
' Find the matching employees.
Dim matches As New List(Of Employee)()
For Each employee As Employee In employees
If employee.LastName.StartsWith("D") Then
matches.Add(employee)
End If
Next
You can then carry on to perform another task with the collection of matches or display it in a web page, as shown here:
gridEmployees.DataSource = matches
gridEmployees.DataBind()
Essentially, LINQ to Objects allows you to replace iterative logic (such as a For Each block) with a declarative expression.
The following example shows how you can rewrite the earlier example,
replacing the For Each block with a LINQ expression that queries the
collection:
' Create the source collection.
Dim employees As New List(Of Employee)()
' (Code for filling the collection omitted to save space.)
Dim matches = From employee In employees
Where employee.LastName.StartsWith("D")
Select employee
gridEmployees.DataSource = matches
gridEmployees.DataBind()
The end result is essentially
the same—you wind up with a collection named matches that's filled with
employees who have last names starting with D, which is then displayed in a grid (see Figure 1).
The LINQ expression uses a
set of new keywords, including From, In, Where, and Select. You shape
your query using these keywords. (You'll see some of the rules of
expression building starting in the next section.)
The
VB compiler is intelligent enough to figure out that the LINQ
expression is a single code statement, even though it's spread over
multiple lines. You could add an underscore at the end of each line to
make this detail explicit, but the code is cleaner if you don't.
LINQ expressions return an usual type of object, called an iterator object.
(In this example, the iterator object is named matches.) Although the
iterator object looks like an ordinary collection to your code, it
doesn't actually hold any information. Instead, it has the ability to
fetch the data when you need it. So when you examine the contents of an
iterator object with a For Each block or when you bind it to a control,
LINQ evaluates your expression and quickly grabs the information you
need. This trick is called deferred execution.
NOTE
There's no technical
reason why LINQ needs to use deferred execution, but there are many
reasons why it's a good approach. In many cases, it allows LINQ to use
performance optimization techniques that wouldn't otherwise be possible.
For example, when using database relationships with LINQ to Entities,
you can avoid loading related data that you don't actually use.
In this example,
the iterator object (named matches) is defined without using an
explicit data type. This is a shortcut that tells the VB compiler to use
the correct data type, without forcing you to specify it. Technically,
the iterator object could be one of several different types of objects
depending on the clauses you use in the LINQ expression. But all of
these objects implement the strongly typed version of the IEnumerable
interface. In this example, that means you can explicitly use the data
type IEnumerable(Of Employee), because the collection holds Employee
objects, if you don't mind the additional complexity. However, either
way the compiled code is exactly the same. If you leave out the data
type, the compiler adds the data type information to your compiled page
automatically (although you never see this detail).
You don't need to know the
specific iterator class that your code uses because you interact with
the results through the strongly typed IEnumerable interface. But if
you're curious, you can determine the object type at runtime using the
Visual Studio debugger (just hover over the variable while in break
mode).
|
|
At
this point, you might be wondering how LINQ actually does its filtering
work. The answer depends on the type of data you're querying. For
example, LINQ to Entities transforms LINQ expressions into database
commands. As a result, the LINQ to Entities plumbing needs to open a
connection and execute a database query to get the data you're
requesting. But if you're using LINQ to Objects, as in the previous
example, the process that LINQ performs is much simpler. In fact, in
this case, LINQ simply uses a For Each loop to scan through your
collections, traveling sequentially from start to finish. Although this
isn't any different from the approach you used in the first place, it
does open up many more possibilities as you use more complex
expressions.
2.1. LINQ Expressions
Before you can go much further
with LINQ, you need to understand how a LINQ expression is composed.
LINQ expressions have a superficial similarity to SQL queries, although
the order of the clauses is rearranged.
All LINQ expressions must
have a From clause that indicates the data source and a Select clause
that indicates the data you want to retrieve (or a Group clause that
defines a series of groups into which the data should be placed). The
From clause is placed first:
Dim matches = From employee In employees
...
The From clause identifies
two pieces of information. The word immediately after In identifies the
data source—in this case, it's the collection object named employees
that holds the EmployeeDetails instances. The word immediately after
From assigns an alias that represents individual items in the data
source. For the purpose of the current expression, each EmployeeDetails
object is named employee. You can then use this alias later when you
build other parts of the expression, such as the filtering and selection
clauses.
Here's the simplest possible LINQ query. It simply retrieves the full set of data from the employees collection:
Dim matches = From employee In employees
Select employee
The VB language includes many
more LINQ operators that won't be considered in detail in this book. In
the following sections, you'll tackle the most important operators,
including Select, Where, and OrderBy. You can review all the LINQ
operators in the Visual Studio Help. You can also find a wide range of
expression examples on Microsoft's 101 LINQ Samples page at http://msdn.microsoft.com/vbasic/bb688088.aspx.
2.1.1. Projections
You can change the Select clause
to get a subset of the data. For example, you could pull out a list of
first name strings like this:
Dim matches = From employee In employees
Select employee.FirstName
or a list of strings with both first and last names:
Dim matches = From employee In employees
Select employee.FirstName + employee.LastName
As shown here, you can use
standard VB operators on numeric data or strings to modify the
information as you're selecting it. This changes the type of collection
that's returned—it's now an IEnumerable(Of String) collection of
strings, rather than a collection of Employee objects. But because this
code defines the matches variable without an indicated data type, the
code keeps working without a hitch.
Even more interestingly, you
can dynamically define a new class that wraps just the information you
want to return. For example, if you want to get both the first and last
names but you want to store them in separate strings, you could create a
stripped-down version of the EmployeeDetails class that includes just a
FirstName and LastName property. To do so, you use a VB feature known
as anonymous types.
The basic technique is to add the New With keywords to the Select
clause, followed by a pair of curly braces. Then, inside the braces, you
assign each property you want to create in terms of the object you're
selecting.
Here's an example:
Dim matches = From employee In employees
Select New
With {.First = employee.FirstName, .Last = employee.LastName}
This expression, when executed,
returns a set of objects that use an implicitly created class. Each
object has two properties: First and Last. You never see the class
definition, because it's generated by the compiler and given a
meaningless, automatically created name. (And for that reason, you can't
pass instances of the automatically generated class to other parts of
your code.) However, you can still use the class locally, access the
First and Last properties, and even use it with data binding (in which
case ASP.NET extracts the appropriate values by property name, using
reflection). The ability to transform the data you're querying into
results with a different structure is called projection.
Figure 2 shows the result of binding the matches collection to a GridView.
Of course, you don't need
to use anonymous types when you perform a projection. You can define the
type formally and then use it in your expression. For example, if you
created the following EmployeeName class:
Public Class EmployeeName
Public Property FirstName() As String
Public Property LastName() As String
End Class
you could change EmployeeDetails objects into EmployeeName objects in your query expression like this:
Dim matches = From employee In employees
Select New EmployeeName With
{.FirstName = employee.FirstName, .LastName = employee.LastName}
This query expression
works because the FirstName and LastName properties are publicly
accessible and aren't read-only. After creating the EmployeeName object,
LINQ sets these properties. Alternatively, you could add a set of
parentheses after the EmployeeName class name and supply arguments for a
parameterized constructor, like this:
Dim matches = From employee In employees
Select New EmployeeName(FirstName, LastName)
2.1.2. Filtering and Sorting
In the first LINQ example in
this article, you saw how a where clause can filter the results to
include only those that match a specific condition. For example, you can
use this code to find employees who have a last name that starts with a
specific letter:
Dim matches = From employee In employees
Where employee.LastName.StartsWith("D")
Select employee
The Where clause takes a
conditional expression that's evaluated for each item. If it's True, the
item is included in the result. However, LINQ keeps the same deferred
execution model, which means the where clause isn't evaluated until you
actually attempt to iterate over the results.
As you probably already expect,
you can combine multiple conditional expressions with the And and Or
operators, and you can use relational operators (such as <, <=,
>, and >=) in conjunction with hard-coded values or other
variables. For example, you could create a query like this to filter out
products greater than a certain price threshold:
Dim matches = From product In products
Where product.UnitsInStock > 0 And product.UnitPrice > 3.00
Select product
One interesting feature of LINQ
expressions is that you can easily call your own methods inline. For
example, you could create a function named TestEmployee() that examines
an employee and returns True or False based on whether you want to
include it in the results:
Private Function TestEmployee(ByVal employee As Employee) As Boolean
Return employee.LastName.StartsWith("D")
End Function
You could then use the TestEmployee() method like this:
Dim matches = From employee In employees
Where TestEmployee(employee)
Select employee
The Order By operator is
equally straightforward. It's modeled after the syntax of the Select
statement in SQL. You simply provide a list of one or more values to use
for sorting, separated by commas.
Here's a basic sorting example:
Dim matches = From employee In employees
Select employee
Order By employee.LastName, employee.LastName
You can also add the word Descending after a field name to sort in the reverse order:
Dim matches = From employee In employees
Select employee
Order By employee.LastName Descending, employee.LastName Descending
There's far more that you can
learn about LINQ expressions. In fact, entire books have been written on
LINQ alone, including the comprehensive Pro LINQ: Language Integrated Query in C# 2008
(Apress). But you now know the essentials of LINQ expressions, which is
enough to let you use it with another remarkable .NET feature: the
Entity Framework.