Chapter 4. Getting familiar with LINQ to Objects

This chapter covers:

  • The LinqBooks running example
  • Querying collections
  • Using LINQ with ASP.NET and Windows Forms
  • Major standard query operators

In chapter 1 we introduced LINQ, and in chapters 2 and 3 we described new language features and LINQ concepts. We’ll now sample each LINQ flavor in turn. This part focuses on LINQ to Objects. We’ll cover LINQ to SQL in part 3, and LINQ to XML in part 4.

The code samples you’ll encounter in the rest of this book are based on a running example: a book cataloging system. This chapter starts with a description of this example application, its database schema, and its object model.

We’ll use this sample application immediately as a base for discovering LINQ to Objects. We’ll review what can be queried with LINQ to Objects and what operations can be performed.

Most of what we’ll show you in this chapter applies to all LINQ flavors and not just LINQ to Objects. We’ll focus on how to write language-integrated queries and how to use the major standard query operators. The goal of this chapter is that you become familiar with query expressions and query operators, as well as feel comfortable using LINQ features with in-memory object collections.

4.1. Introducing our running example

While we were introducing the new language features (chapter 2) and key LINQ concepts (chapter 3), we used simple code samples. We should now be able to tackle more useful and complex real-life examples. Starting at this point, the new code samples in this book will be based on an ongoing example: LinqBooks, a personal book-cataloging system.

We’ll discuss the goals behind the example and review the features we expect it to implement. We’ll then show you the object model and database schema we’ll use throughout this book. We’ll also introduce sample data we’ll use to create our examples.

4.1.1. Goals

A running example will allow us to base our code samples on something solid. We’ve chosen to develop an example that is rich enough to offer opportunities to use the complete LINQ toolset.

Here are some of our requirements for this example:

  • The object model should be rich enough to enable a variety of LINQ queries.
  • It should deal with objects in memory, XML documents, and relational data, both independently and in combination.
  • It should include ASP.NET web sites as well as Windows Forms applications.
  • It should involve queries to local data stores as well as to external data sources, such as public web services.

Although we may provide a complete sample application after this book is published, our goal here is not to create a full-featured application. However, in chapter 13, we’ll focus on using all the parts of our running example to see LINQ in action in a complete application.

Let’s review the set of features we plan to implement.

4.1.2. Features

The main features LinqBooks should have include the ability to

  • Track what books we have
  • Store what we think about them
  • Retrieve more information about our books
  • Publish our list of books and our review information

The technical features we’ll implement in this book include

  • Querying/inserting/updating data in a local database
  • Providing search capabilities over both the local catalog and third parties (such as Amazon or Google)
  • Importing data about books from a web site
  • Importing and persisting some data from/in XML documents
  • Creating RSS feeds for the books you recommend

In order to implement these features, we’ll use a set of business entities.

4.1.3. The business entities

The object model we’ll use consists of the following classes: Book, Author, Publisher, Subject, Review, and User.

Figure 4.1 is a class diagram that shows how these objects are defined and how they relate to each other.

Figure 4.1. Object model for the running example

We’ll first use these objects in memory with LINQ to Objects, but later on we’ll have to persist this data in a database. Let’s see the database model we’ll use.

4.1.4. Database schema

In part 3 of this book, we’ll demonstrate how to use LINQ to work with relational databases. Figure 4.2 shows the database schema we’ll use.

Figure 4.2. Database schema for the running example

We’ll use this database to save and load the information the application handles. This schema was designed to involve several kinds of relations and data types. This will be useful to demonstrate the features LINQ to SQL offers for dealing with relational data.

4.1.5. Sample data

In this part of the book, we’ll use a set of in-memory data for the purpose of demonstrating LINQ to Objects.

Listing 4.1 contains the SampleData class that contains the data we’ll use.

Listing 4.1. The SampleData class provides sample data (LinqBooks.CommonSampleData.cs)
using System;
using System.Collections.Generic;
using System.Text;

namespace LinqInAction.LinqBooks.Common
{
  static public class SampleData
  {
    static public Publisher[] Publishers =
    {
      new Publisher {Name="FunBooks"},
      new Publisher {Name="Joe Publishing"},
      new Publisher {Name="I Publisher"}
    };

    static public Author[] Authors =
    {
      new Author {FirstName="Johnny", LastName="Good"},
      new Author {FirstName="Graziella", LastName="Simplegame"},
      new Author {FirstName="Octavio", LastName="Prince"},
      new Author {FirstName="Jeremy", LastName="Legrand"}
    };

    static public Book[] Books =
    {
      new Book {
        Title="Funny Stories",
        Publisher=Publishers[0],
        Authors=new[]{Authors[0], Authors[1]},
        PageCount=101,
        Price=25.55M,
        PublicationDate=new DateTime(2004, 11, 10),
        Isbn="0-000-77777-2"
      },
      new Book {
        Title="LINQ rules",
        Publisher=Publishers[1],
        Authors=new[]{Authors[2]},
        PageCount=300,
        Price=12M,
        PublicationDate=new DateTime(2007, 9, 2),
        Isbn="0-111-77777-2"
      },
      new Book {
        Title="C# on Rails",
        Publisher=Publishers[1],
        Authors=new[]{Authors[2]},
        PageCount=256,
        Price=35.5M,
        PublicationDate=new DateTime(2007, 4, 1),
        Isbn="0-222-77777-2"
      },
      new Book {
        Title="All your base are belong to us",
        Publisher=Publishers[1],
        Authors=new[]{Authors[3]},
        PageCount=1205,
        Price=35.5M,
        PublicationDate=new DateTime(2005, 5, 5),
        Isbn="0-333-77777-2"
      },
      new Book {
        Title="Bonjour mon Amour",
        Publisher=Publishers[0],
        Authors=new[]{Authors[1], Authors[0]},
        PageCount=50,
        Price=29M,
        PublicationDate=new DateTime(1973, 2, 18),
        Isbn="2-444-77777-2"
      }
    };
  }
}

Notice how we use object and collection initializers—introduced in chapter 2—to easily initialize our collections. This sample data and the classes it relies on are provided with the source code of this book in the LinqBooks.Common project.

When we address LINQ to XML and LINQ to SQL, we’ll use a set of sample XML documents and sample records in a database. We’ll show you this additional data when we use it.

Before using this sample data and actually working with our running example, we’ll review some basic information about LINQ to Objects.

4.2. Using LINQ with in-memory collections

LINQ to Objects is the flavor of LINQ that works with in-memory collections of objects. What does this mean? What kinds of collections are supported by LINQ to Objects? What operations can we perform on these collections?

We’ll start by reviewing the list of collections that are compatible with LINQ, and then we’ll give you an overview of the supported operations.

4.2.1. What can we query?

As you might guess, not everything can be queried using LINQ to Objects. The first criterion for applying LINQ queries is that the objects need to be collections.

All that is required for a collection to be queryable through LINQ to Objects is that it implements the IEnumerable<T> interface. As a reminder, objects implementing the IEnumerable<T> interface are called sequences in LINQ vocabulary. The good news is that almost every generic collection provided by the .NET Framework implements IEnumerable<T>! This means that you’ll be able to query the usual collections you were already working with in .NET 2.0.

Let’s review the collections you’ll be able to query using LINQ to Objects.

Arrays

Any kind of array is supported. It can be an untyped array of objects, like in listing 4.2.

Listing 4.2. Querying an untyped array with LINQ to Objects (UntypedArray.csproj)
using System;
using System.Linq;

static class TestArray
{
  static void Main()
  {
    Object[] array = {"String", 12, true, 'a'};
    var types =
      array
        .Select(item => item.GetType().Name)
        .OrderBy(type => type);

    ObjectDumper.Write(types);
  }
}

 

Note

We already used the ObjectDumper class in chapter 2. It is a utility class useful for displaying results. It is provided by Microsoft as part of the LINQ code samples. You’ll be able to find it in the downloadable source code accompanying this book.

 

This code displays the types of an array’s elements, sorted by name. Here is the output of this example:

Boolean
Char
Int32
String

Of course, queries can be applied to arrays of custom objects. In listing 4.3, we query an array of Book objects.

Listing 4.3. Querying a typed array with LINQ to Objects (TypedArray.csproj)
using System;
using System.Collections.Generic;
using System.Linq;
using LinqInAction.LinqBooks.Common;

static class TestArray
{
  static void Main()
  {
    Book[] books = {
      new Book { Title="LINQ in Action" },
      new Book { Title="LINQ for Fun" },
      new Book { Title="Extreme LINQ" } };

    var titles =
      books
        .Where(book => book.Title.Contains("Action"))
        .Select(book => book.Title);

    ObjectDumper.Write(titles);
  }
}

In fact, LINQ to Objects queries can be used with an array of any data type!

Other important collections, such as generic lists and dictionaries, are also supported by LINQ to Objects. Let’s see what other types you can use.

Generic lists

The most common collection you use in .NET 2.0 with arrays is without a doubt the generic List<T>. LINQ to Objects can operate on List<T>, as well as on the other generic lists.

Here is a list of the main generic list types:

  • System.Collections.Generic.List<T>
  • System.Collections.Generic.LinkedList<T>
  • System.Collections.Generic.Queue<T>
  • System.Collections.Generic.Stack<T>
  • System.Collections.Generic.HashSet<T>
  • System.Collections.ObjectModel.Collection<T>
  • System.ComponentModel.BindingList<T>

Listing 4.4 shows how the previous example that worked with an array can be adapted to work with a generic list.

Listing 4.4. Querying a generic list with LINQ to Objects (GenericList.csproj)
using System;
using System.Collections.Generic;
using System.Linq;
using LinqInAction.LinqBooks.Common;

static class TestList
{
  static void Main()
  {
    List<Book> books = new List<Book>() {
      new Book { Title="LINQ in Action" },
      new Book { Title="LINQ for Fun" },
      new Book { Title="Extreme LINQ" } };

    var titles =
      books
        .Where(book => book.Title.Contains("Action"))
        .Select(book => book.Title);

    ObjectDumper.Write(titles);
  }
}

Note that the query remains unchanged, because both the array and the list implement the same interface used by the query: IEnumerable<Book>.

Although you’ll most likely primarily query arrays and lists with LINQ, you may also write queries against generic dictionaries.

Generic dictionaries

As with generic lists, all generic dictionaries can be queried using LINQ to Objects:

  • System.Collections.Generic.Dictionary<TKey, TValue>
  • System.Collections.Generic.SortedDictionary<TKey, TValue>
  • System.Collections.Generic.SortedList<TKey, TValue>

Generic dictionaries implement IEnumerable<KeyValuePair<TKey, TValue>>. The KeyValuePair structure holds the typed Key and Value properties.

Listing 4.5 shows how we can query a dictionary of strings indexed by integers.

Listing 4.5. Querying a generic dictionary with LINQ to Objects (GenericDictionary.csproj)
using System;
using System.Collections.Generic;
using System.Linq;

static class TestDictionary
{
  static void Main()
  {
      Dictionary<int, string> frenchNumbers;
      frenchNumbers = new Dictionary<int, string>();
      frenchNumbers.Add(0, "zero");
      frenchNumbers.Add(1, "un");
      frenchNumbers.Add(2, "deux");
      frenchNumbers.Add(3, "trois");
      frenchNumbers.Add(4, "quatre");

      var evenFrenchNumbers =
        from entry in frenchNumbers
        where (entry.Key % 2) == 0
        select entry.Value;

    ObjectDumper.Write(evenFrenchNumbers);
  }
}

Here is the output of this sample’s execution:

zero
deux
quatre

We’ve listed the most important collections you’ll query. You can query other collections, as you’ll see next.

String

Although System.String may not be perceived as a collection at first sight, it actually is one, because it implements IEnumerable<Char>. This means that string objects can be queried with LINQ to Objects, like any other collection.

 

Note

In C#, these extension methods will not be seen in IntelliSense. The extension methods for System.String are specifically excluded because it is seen as highly unusual to treat a string object as an IEnumerable<char>.

 

Let’s take an example. The LINQ query in listing 4.6 works on the characters from a string.

Listing 4.6. Querying a string with LINQ to Objects (String.csproj)
var count =
  "Non-letter characters in this string: 8"
    .Where(c => !Char.IsLetter(c))
    .Count();

Needless to say, the result of this query is 8.

Other collections

We’ve listed only the collections provided by the .NET Framework. Of course, you can use LINQ to Objects with any other type that implements IEnumerable<T>. This means LINQ to Objects will work with your own collection types or collections from other frameworks.

A problem you may encounter is that not all .NET collections implement IEnumerable<T>. In fact, only strongly typed collections implement this interface. Arrays, generic lists, and generic dictionaries are strongly typed: you can work with an array of integers, a list of strings, or a dictionary of Book objects.

The nongeneric collections do not implement IEnumerable<T>, but implement IEnumerable. Does this mean that you won’t be able to use LINQ with DataSet or ArrayList objects, for example?

Fortunately, solutions exist. In section 5.1.1, we’ll demonstrate how you can query nongeneric collections thanks to the Cast and OfType query operators.

Let’s now review what LINQ allows us to do with all these collections.

4.2.2. Supported operations

The operations that can be performed on the types we’ve just listed are those supported by the standard query operators. LINQ comes with a number of operators that provide useful ways of manipulating sequences and composing queries.

Here is an overview of the families of the standard query operators: Restriction, Projection, Partitioning, Join, Ordering, Grouping, Set, Conversion, Equality, Element, Generation, Quantifiers, and Aggregation. As you can see, a wide range of operations is supported. We won’t detail all of them, but we’ll focus on the most important of them in section 4.4.

Remember that the standard query operators are defined in the System.Linq.Enumerable class as extension methods for the IEnumerable<T> type, as we’ve seen in chapter 3.

These operators are called the standard query operators because we can provide our own custom query operators. Because query operators are merely extension methods for the IEnumerable<T> type, we’re free to create all the query operators we wish. This allows us to enrich our queries with operations that the designers of LINQ overlooked and that aren’t supported by the standard operators. We’ll demonstrate this in chapter 12 when we cover extensibility.

We’ll soon use several query operators and demonstrate how to perform the supported operations we’ve just presented. In order to be able to create our sample applications, we’ll now take some time to create our first ASP.NET web sites and Windows Forms applications that work with LINQ.

4.3. Using LINQ with ASP.NET and Windows Forms

In previous chapters, we used LINQ code in console applications. That was okay for simple examples, but most real-life projects take the form of web sites or Windows applications, not console applications. We’ll now make the jump and start creating ASP.NET or Windows Forms applications that use LINQ.

Support for LINQ is built into .NET 3.5 and Visual Studio 2008, so creating applications that use LINQ is not different than creating other applications. You simply need to use the standard project templates coming with Visual Studio. This is the case for both ASP.NET web sites and Windows Forms applications. We’ll show you how to use these templates to create your first applications that query data using LINQ and display the results using standard .NET controls.

 

Note

If you used prerelease versions of LINQ, you may remember using specific project templates. The standard templates that come with Visual Studio 2008 now support LINQ. The project templates create the required references to the LINQ assemblies. Of course, this is true only if you select .NET Framework 3.5 as the target for your project, the default value.

 

4.3.1. Data binding for web applications

ASP.NET controls support data binding to any IEnumerable collection. This makes it easy to display the result of language-integrated queries using controls like GridView, DataList, and Repeater.

Let’s create a sample web site and improve it step by step.

Step 0: Creating an ASP.NET web site

To create a new ASP.NET web site, choose File > New > Web Site in Visual Studio, and select the ASP.NET Web Site template, as shown in figure 4.3.

Figure 4.3. Creating a new ASP.NET web site

This creates a web site project that looks like figure 4.4.

Figure 4.4. Default content for a web site

We’ll add a new page to this project to display some data.

Step 1: Creating our first ASP.NET page using LINQ

Create a new page called Step1.aspx and add a GridView control to it so it looks like listing 4.7.

Listing 4.7. Markup for the first ASP.NET page (Step1.aspx)
<%@ Page Language="C#" AutoEventWireup="true"
  CodeFile="Step1.aspx.cs" Inherits="Step1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Step 1</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:GridView ID="GridView1" runat="server">
      </asp:GridView>
    </div>
    </form>
</body>
</html>

Listing 4.8 contains the code you should write in the code-behind file to bind a query to the GridView.

Listing 4.8. Code-behind for the first ASP.NET page (Step1.aspx.cs)
using System;
using System.Linq;

using LinqInAction.LinqBooks.Common;

public partial class Step1 : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    String[] books = { "Funny Stories",
      "All your base are belong to us", "LINQ rules",
      "C# on Rails", "Bonjour mon Amour" };

    GridView1.DataSource =
      from book in books
      where book.Length > 10
      orderby book
      select book.ToUpper();
    GridView1.DataBind();
  }
}

Make sure you have a using System.Linq statement at the top of the file to ensure we can use LINQ querying features.

Here, we use a query expression, a syntax we introduced in chapter 3. The query selects all the books with names longer than 10 characters, sorts the result in alphabetical order, then returns the names converted into uppercase.

LINQ queries return results of type IEnumerable<T>, where T is determined by the object type of the select clause. In this sample, book is a string, so the result of the query is a generics-based collection of type IEnumerable<String>.

Because ASP.NET controls support data binding to any IEnumerable collection, we can easily assign this LINQ query to the GridView control. Calling the DataBind method on the GridView generates the display.

The result page looks like figure 4.5 when the application is run.

Figure 4.5. ASP.NET step 1 result

 

Note

Instead of using the GridView control, you can use as easily a Repeater, DataList, DropDownList, or any other ASP.NET list control. This includes the new ListView control that comes with .NET 3.5.

You could also use the new LinqDataSource control to enable richer data binding. You’ll be able to see it in action in the last chapter of this book, when we create the LinqBooks web application.

 

That’s it! We’ve created our first ASP.NET web site that uses LINQ. Not terribly difficult, right? Let’s improve our example a bit, because everything is so easy.

Step 2: Using richer collections

Searching an array of strings is not extremely interesting (although sometimes useful). To make our application more realistic, let’s add the ability to search and work against richer collections. The good news is that LINQ makes this easy.

Let’s use the types and sample data from our running example. For instance, we could query our collection of books filtered and ordered on prices. We’d like to achieve something like figure 4.6.

Figure 4.6. Result of using richer collections in ASP.NET

Notice that this time we’re also displaying the price. Title and Price are two properties of our Book object. A Book object has more than these two properties, as you can see in figure 4.7.

Figure 4.7. The Book class

We can use two methods to display only the properties we want: either declare specific columns at the grid level, or explicitly select only the Title and Price properties in the query.

Let’s try the former method first.

In order to use the Book class and the sample data provided with this book, start by adding a reference to the LinqBooks.Common project. Then, create a new page named Step2a.aspx with a GridView control that defines two columns, as in listing 4.9.

Listing 4.9. Markup for a richer collection (Step2a.aspx)
<%@ Page Language="C#" AutoEventWireup="true"
  CodeFile="Step2a.aspx.cs" Inherits="Step2a" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Step 2 – Grid columns</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:GridView ID="GridView1" runat="server"
        AutoGenerateColumns="false">
        <Columns>
          <asp:BoundField HeaderText="Book" DataField="Title" />
          <asp:BoundField HeaderText="Price" DataField="Price" />
        </Columns>
      </asp:GridView>
    </div>
    </form>
</body>
</html>

Listing 4.10 shows the new query that works on our sample data and returns Book objects.

Listing 4.10. Code-behind for a richer collection (Step2a.aspx.cs)
protected void Page_Load(object sender, EventArgs e)
{
  GridView1.DataSource =
    from book in SampleData.Books
    where book.Title.Length > 10
    orderby book.Price
    select book;
  GridView1.DataBind();
}

Make sure there is a using System.Linq statement at the top of the file.

The GridView displays only the two properties specified as columns because we’ve specified that we don’t want it to generate columns automatically based on the properties of the objects.

As we said, another way to specify the columns displayed in the grid is to select only the properties we want in the query. This is what we do in listing 4.11.

Listing 4.11. Code-behind for a richer collection using an anonymous type (Step2b.aspx.cs)
using System;
using System.Linq;

using LinqInAction.LinqBooks.Common;

public partial class Step2b : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    GridView1.DataSource =
      from book in SampleData.Books
      where book.Title.Length > 10
      orderby book.Price
      select new { book.Title, book.Price };
    GridView1.DataBind();
  }
}

As you can see, this is done using an anonymous type, a language extension we introduced in chapter 2. Anonymous types allow you to easily create and use type structures inline, without having to formally declare their object model beforehand. A type is automatically inferred by the compiler based on the initialization data for the object.

Instead of returning a Book object from our select clause like before, we’re now creating a new anonymous type that has two properties—Title and Price. The types of these properties are automatically calculated based on the value of their initial assignment (in this case a String and a Decimal).

This time, thanks to the anonymous type, we don’t need to specify the columns in the grid: See listing 4.12.

 

Note

Keep in mind that the columns in the grid may not appear in the order you expect. The GridView control relies on reflection to get the properties of the objects it should display. This technique does not ensure that the properties are returned in the same order as they are declared in the bound object.

 

Listing 4.12. Markup for listing 4.11 (Step2b.aspx)
...
<body>
    <form id="form1" runat="server">
    <div>
      <asp:GridView ID="GridView1" runat="server"
        AutoGenerateColumns="true">
      </asp:GridView>
    </div>
    </form>
</body>
</html>

Both of the methods we’ve just presented to limit the number of columns are useful. The first method allows us to specify header text or other options for the columns. For instance, here we used “Book” as the header for the column that displays the title. The second method allows us to select only the data we need and not the complete objects. This will be useful especially when working with LINQ to SQL, as you’ll see in part 3 of this book, to avoid retrieving too much data from the database server.

An even more important benefit of using anonymous types is that you can avoid having to create new types just for presenting data. In trivial situations, you can use an anonymous type to map your domain model to a presentation model. In the following query, creating an anonymous type allows a flat view of our domain model:

from book in SampleData.Books
where book.Title.Length > 10
orderby book.Price
select new { book.Title, book.Price,
  Publisher=book.Publisher.Name, Authors=book.Authors.Count()  };

Here we create a view on a graph of objects by projecting data from the object itself and data from the object’s relations into an anonymous type.

After creating an ASP.NET site, let’s see how to do the same with Windows Forms.

4.3.2. Data binding for Windows Forms applications

Using LINQ in a Windows Forms application isn’t more difficult than with ASP.NET in a web application. We’ll show you how to do the same kind of data-binding operations between LINQ query results and standard Windows Forms controls in a sample application.

We’ll proceed the same way we did with ASP.NET. We’ll build a sample application step by step, starting with the creation of a new project.

Step 0: Creating a Windows Forms application

To create a new Windows Application, choose File > New > Project, and select Windows Forms Application, as shown in figure 4.8.

Figure 4.8. Visual Studio 2008’s new project dialog box

Figure 4.9 shows the default content created by this template.

Figure 4.9. Default content for a new Windows Forms application

Step 1: Creating our first form using LINQ

We’ll start our sample by creating a new form for displaying books returned by a query. Create a form named FormStrings, and drop a DataGridView control on it, as shown in figure 4.10.

Figure 4.10. New form with a DataGridView

Add an event handler for the Load event of the page as in listing 4.13.

Listing 4.13. Code-behind for the first form (FormStrings.cs)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace LinqInAction.Chapter04.Win
{
  public partial class FormStrings : Form
  {
    public FormStrings()
    {
      InitializeComponent();
    }

    private void FormStrings_Load(object sender, EventArgs e)
    {
      String[] books = { "Funny Stories",
        "All your base are belong to us", "LINQ rules",
        "C# on Rails", "Bonjour mon Amour" };


      var query =
        from book in books
        where book.Length > 10
        orderby book
        select new { Book=book.ToUpper() };

      dataGridView1.DataSource = query.ToList();
    }
  }
}

Make sure you import the System.Linq namespace with a using clause.

You should notice two things in comparison to the code we used for the ASP.NET web application sample in section 4.3.1. First, we use an anonymous type to create objects containing a Book property. This is because the DataGridView control displays the properties of objects by default. If we returned strings instead of custom objects, all we would see displayed would be the title’s Length, because that’s the only property on strings. Second, we convert the result sequence into a list. This is required for the grid to perform data binding. Alternatively, we could use a BindingSource object.

Figure 4.11 shows the result of this code sample’s execution.

Figure 4.11. Result of the first Windows Forms step

This is not perfect, because the titles are not completely displayed. We’ll improve this in the next step, while we display more information at the same time.

Step 2: Using richer collections

As we did for ASP.NET, we’ll now use richer objects and not just strings. We’ll reuse the same sample data from our running example, so make sure you reference the LinqBooks.Common project.

Figure 4.12 shows the result we’d like to get with a query that filters and sorts our book collection.

Figure 4.12. Result of the second Windows Forms step

To achieve this result, first create a new form named FormBooks. Add a DataGridView control to it, just like you did for the previous sample.

This time, we’ll specify the grid columns. Edit the columns using the grid’s smart tags, as shown in figure 4.13.

Figure 4.13. DataGridView’s smart tags

Add two columns, Book and Price, as shown in figure 4.14.

Figure 4.14. Adding two columns to the DataGridView control

Note that we can also specify the width of each column. We could for example specify that we wish the columns to be automatically sized according to their content, using the AutoSizeMode setting.

That’s all there is to it. We now have a rich collection mapped to a grid.

Because you now have some knowledge of data binding of LINQ queries in web and Windows applications, let’s move on to building richer examples. We’ll use the data binding techniques we just showed you to write advanced queries. You’ll see how to use the query operators to perform several kinds of common operations, such as projections or aggregations.

Make sure you map the columns to the result objects’ properties using the DataPropertyName setting, as shown in figure 4.15.

Figure 4.15. Mapping columns to properties and specifying column width

4.4. Focus on major standard query operators

Before using query expressions and query operators to start creating the sample application we introduced at the beginning of this chapter, we’ll take a small detour to focus on some of the standard query operators. It’s important to know the standard query operators because they are the elements that make queries. You need to get a good idea of the existing operators and what they can be used for.

We won’t be able to cover all of the 51 standard query operators, but only a subset of them. We’ll highlight the major operators like Where, Select, SelectMany, the conversion operators, and some aggregation operators. Don’t worry—you’ll see many of the other standard query operators in action throughout the code samples contained in this book.

As a reminder, table 4.1 lists all the standard query operators.

Table 4.1. The standard query operators grouped in families

Family

Query operators

Filtering OfType, Where
Projection Select, SelectMany
Partitioning Skip, SkipWhile, Take, TakeWhile
Join GroupJoin, Join
Concatenation Concat
Ordering OrderBy, OrderByDescending, Reverse, ThenBy, ThenByDescending
Grouping GroupBy, ToLookup
Set Distinct, Except, Intersect, Union
Conversion AsEnumerable, AsQueryable, Cast, ToArray, ToDictionary, ToList
Equality SequenceEqual
Element ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault
Generation DefaultIfEmpty, Empty, Range, Repeat
Quantifiers All, Any, Contains
Aggregation Aggregate, Average, Count, LongCount, Max, Min, Sum

The operators covered in this chapter are highlighted in bold text. We’ll let you discover the others by yourself.[1] Once we’ve shown you about half of the operators in this chapter, it should be easier to learn new ones. You’ll see most of them in action in the rest of this book, even if we don’t provide full details about them.

1 The complete list of the standard query operators with their descriptions is available in the appendix.

Let’s start our exploration of the query operators with Where.

4.4.1. Where, the restriction operator

Similar to a sieve, the Where operator filters a sequence of values based on some criteria. Where enumerates a source sequence yielding only those values that match the predicate you provide.

Here is how the Where operator is declared:

public static IEnumerable<T> Where<T>(
  this IEnumerable<T> source,
  Func<T, bool> predicate);

The first argument of the predicate function represents the element to test. This function returns a Boolean value indicating whether test conditions are satisfied.

The following example creates a sequence of the books that have a price greater than or equal to 15:

IEnumerable<Book> books =
  SampleData.Books.Where(book => book.Price >= 15);

In a query expression, a where clause translates to an invocation of the Where operator. The previous example is equivalent to the translation of the following query expression:

var books =
  from book in SampleData.Books
  where book.Price >= 15
  select book;

An overload of the Where operator uses predicates that work with the index of elements in the source sequence:

public static IEnumerable<T> Where<T>(
  this IEnumerable<T> source,
  Func<T, int, bool> predicate);

The second argument of the predicate, if present, represents the zero-based index of the element within the source sequence.

The following code snippet uses this version of the operator to filter the collection of books and keep only those that have a price greater than or equal to 15 and are in odd positions (should you wish to do so for some strange reason):

IEnumerable<Book> books =
  SampleData.Books.Where(
    (book, index) => (book.Price >= 15) && ((index & 1) == 1));

Where is a restriction operator. It’s simple, but you’ll use it often to filter sequences.

Another operator you’ll use often is Select.

4.4.2. Using projection operators

Let’s review the two projection operators: Select and SelectMany.

Select

The Select operator is used to perform a projection over a sequence, based on the arguments passed to the operator. Select is declared as follows:

public static IEnumerable<S> Select<T, S>(
  this IEnumerable<T> source,
  Func<T, S> selector);

The Select operator allocates and yields an enumeration, based on the evaluation of the selector function applied to each element of the source enumeration. The following example creates a sequence of the titles of all books:

IEnumerable<String> titles =
  SampleData.Books.Select(book => book.Title);

In a query expression, a select clause translates to an invocation of Select. The following query expression translates to the preceding example:

var titles =
  from book in SampleData.Books
  select book.Title;

This query narrows a sequence of books to a sequence of string values. We could also select an object. Here is how we would select Publisher objects associated with books:

var publishers =
  from book in SampleData.Books
  select book.Publisher;

The resulting collection of using Select can also be a direct pass-through of the source objects, or any combination of fields in a new object. In the following sample, an anonymous type is used to project information into an object:

var books =
  from book in SampleData.Books
  select new { book.Title, book.Publisher.Name, book.Authors };

This kind of code creates a projection of data, hence the name of this operator’s family. Let’s take a look at the second projection operator.

SelectMany

The second operator in the projection family is SelectMany. Its declaration is similar to that of Select, except that its selector function returns a sequence:

public static IEnumerable<S> SelectMany<T, S>(
  this IEnumerable<T> source,
  Func<T, IEnumerable<S>> selector);

The SelectMany operator maps each element from the sequence returned by the selector function to a new sequence, and concatenates the results. To understand what SelectMany does, let’s compare its behavior with Select in the following code samples.

Here is some code that uses the Select operator:

IEnumerable<IEnumerable<Author>> tmp =
  SampleData.Books
    .Select(book => book.Authors);
foreach (var authors in tmp)
{
  foreach (Author author in authors)
  {
    Console.WriteLine(author.LastName);
  }
}

And here’s the equivalent code using SelectMany. As you can see, it is much shorter:

IEnumerable<Author> authors =
  SampleData.Books
    .SelectMany(book => book.Authors);
foreach (Author author in authors)
{
  Console.WriteLine(author.LastName);
}

Here we’re trying to enumerate the authors of our books. The Authors property of the Book object is an array of Author objects. Therefore, the Select operator returns an enumeration of these arrays as is. In comparison, SelectMany spreads the elements of these arrays into a sequence of Author objects.

Here is the query expression we could use in place of the SelectMany invocation in our example:

from book in SampleData.Books
from author in book.Authors
select author.LastName

Notice how we chain two from clauses. In a query expression, a SelectMany projection is involved each time from clauses are chained. When we cover the join operators in section 4.5.4, we’ll show you how this can be used to perform a cross join.

The Select and SelectMany operators also provide overloads that work with indices. Let’s see what they can be used for.

Selecting indices

The Select and SelectMany operators can be used to retrieve the index of each element in a sequence. Let’s say we want to display the index of each book in our collection before we sort them in alphabetical order:

index=3         Title=All your base are belong to us
index=4         Title=Bonjour mon Amour
index=2         Title=C# on Rails
index=0         Title=Funny Stories
index=1         Title=LINQ rules

Listing 4.14 shows how to use Select to achieve that.

Listing 4.14. Sample use of the Select query operator with indices (SelectIndex.csproj)
var books =
  SampleData.Books
    .Select((book, index) => new { index, book.Title })
    .OrderBy(book => book.Title);
ObjectDumper.Write(books);

This time we can’t use the query expression syntax because the variant of the Select operator that provides the index has no equivalent in this syntax. Notice that this version of the Select method provides an index variable that we can use in our lambda expression. The compiler automatically determines which version of the Select operator we want just by looking at the presence or absence of the index parameter. Note also that we call Select before OrderBy. This is important to get the indices before the books are sorted, not after.

Let’s now review another query operator: Distinct.

4.4.3. Using Distinct

Sometimes, information is duplicated in query results. For example, listing 4.15 returns the list of authors who have written books.

Listing 4.15. Retrieving a list of authors without using the Distinct query operator (Distinct.csproj)
var authors =
  SampleData.Books
    .SelectMany(book => book.Authors)
    .Select(author => author.FirstName+" "+author.LastName);
ObjectDumper.Write(authors);

You can see that a given author may appear more than once in the results:

Johnny Good
Graziella Simplegame
Octavio Prince
Octavio Prince
Jeremy Legrand
Graziella Simplegame
Johnny Good

This is because an author can write several books. To remove duplication, we can use the Distinct operator. Distinct eliminates duplicate elements from a sequence. In order to compare the elements, the Distinct operator uses the elements’ implementation of the IEquatable<T>.Equals method if the elements implement the IEquatable<T> interface. It uses their implementation of the Object.Equals method otherwise.

Listing 4.16 does not yield the same author twice.

Listing 4.16. Retrieving a list of authors using the Distinct query operator (Distinct.csproj)
var authors =
  SampleData.Books
    .SelectMany(book => book.Authors)
    .Distinct()
    .Select(author => author.FirstName+" "+author.LastName);
ObjectDumper.Write(authors);

The new result is:

Johnny Good
Graziella Simplegame
Octavio Prince
Jeremy Legrand

As with many query operators, there is no equivalent keyword for Distinct in the C# query expression syntax. In C#, Distinct can only be used as a method call. However, VB.NET offers support for the Distinct operator in query expressions. Listing 4.17 shows how the query from listing 4.16 can be written in VB.NET.

Listing 4.17. Retrieving a list of authors using the VB Distinct keyword (Distinct.vbproj)
Dim authors = _
  From book In SampleData.Books _
  From author In book.Authors _
  Select author.FirstName + " " + author.LastName _
  Distinct

The next family of operators that we’re going to explore does not have equivalent keywords in query expressions, either in C# or in VB.NET. These operators can be used to convert sequences to standard collections.

4.4.4. Using conversion operators

LINQ comes with convenience operators designed to convert a sequence to other collections. The ToArray and ToList operators, for instance, convert a sequence to a typed array or list, respectively. These operators are useful for integrating queried data with existing code libraries. They allow you to call methods that expect arrays or list objects, for example.

By default, queries return sequences, collections implementing IEnumerable<T>:

IEnumerable<String> titles =
  SampleData.Books.Select(book => book.Title);

Here is how such a result can be converted to an array or a list:

String[] array = titles.ToArray();
List<String> list = titles.ToList();

ToArray and ToList are also useful when you want to request immediate execution of a query or cache the result of a query. When invoked, these operators completely enumerate the source sequence on which they are applied to build an image of the elements returned by this sequence.

Remember that, as we showed you in chapter 3, a query can return different results in successive executions. You’ll use ToArray and ToList when you want to take an instant snapshot of a sequence. Because these operators copy all the result elements into a new array or list each time you call them, you should be careful and avoid abusing them on large sequences.

Let’s consider a use case worth mentioning. If we’re querying a disposable object created by a using block, and if we’re yielding from inside that block, the object will be disposed of before we want it to. The workaround is to materialize the results with ToList, exit the using block, and then yield the results out.

Here is pseudocode that pictures this:

IEnumerable<Book> results;

using (var db = new LinqBooksDataContext())
{
  results = db.Books.Where(...).ToList();
}
foreach (var book in results)
{
  DoSomething(book);
  yield return book;
}

Another interesting conversion operator is ToDictionary. Instead of creating an array or list, this operator creates a dictionary, which organizes data by keys.

Let’s see an example:

Dictionary<String, Book> isbnRef =
  SampleData.Books.ToDictionary(book => book.Isbn);

Here we create a dictionary of books that is indexed by each book’s ISBN. A variable of this kind can be used to find a book based on its ISBN:

Book linqRules = isbnRef["0-111-77777-2"];

After these conversion operators,[2] let’s see one last family: aggregate operators.

2 These conversion operators are demonstrated in ConversionOperators.csproj and ConversionOperators.vbproj.

4.4.5. Using aggregate operators

Some standard query operators are available to apply math functions to data: the aggregate operators. These operators include the following:

  • Count, which counts the number of elements in a sequence
  • Sum, which computes the sum of a sequence of numeric values
  • Min and Max, which find the minimum and the maximum of a sequence of numeric values, respectively

The following example demonstrates how these operators can be used:

var minPrice = SampleData.Books.Min(book => book.Price);
var maxPrice = SampleData.Books.Select(book => book.Price).Max();
var totalPrice = SampleData.Books.Sum(book => book.Price);
var nbCheapBooks =
  SampleData.Books.Where(book => book.Price < 30).Count();

You may have noticed that in this code sample, Min and Max are not invoked in the same way. The Min operator is invoked directly on the book collection, whereas the Max operator is chained after the Select operator. The effect is identical. In the former case, the aggregate function is applied just to the sequences that satisfy the expression; in the latter case it is applied to all the objects. All the aggregate operators can take a selector as a parameter. The choice of one overload or the other depends on whether you’re working on a prerestricted sequence.

We’ve introduced some important query operators. You should now be more familiar with Where, Select, SelectMany, Distinct, ToArray, ToList, Count, Sum, Min, and Max. This is a good start! There are many more useful operators, as you’ll see next.

4.5. Creating views on an object graph in memory

After focusing on the major operators in the previous section, we’ll now use them to discover others in the context of our sample application. We’ll see how to write queries and use the query operators to perform common operations such as sorting, dealing with nested data, and grouping.

Let’s start with sorting.

4.5.1. Sorting

The objects in our sample data come in a specific order. This is an arbitrary order, and we may wish to view the data sorted by specific orderings. Query expressions allow us to use orderby clauses for this.

Let’s return to our web example. Let’s say we’d like to view our books sorted by publisher, then by descending price, and then by ascending title. The result would look like figure 4.16.

Figure 4.16. Sorting result

The query we’d use to achieve this result is shown in listing 4.18.

Listing 4.18. Using an orderby clause to sort results (Sorting.aspx.cs)
from book in SampleData.Books
  orderby book.Publisher.Name, book.Price descending, book.Title
  select new { Publisher=book.Publisher.Name,
               book.Price,
               book.Title };

The orderby keyword can be used to specify several orderings. By default, items are sorted in ascending order. It’s possible to use the descending keyword on a per-member basis, as we do here for the price.

A query expression’s orderby clause translates to a composition of calls to the OrderBy, ThenBy, OrderByDescending, and ThenByDescending operators. Here is our example expressed with query operators:

SampleData.Books
  .OrderBy(book => book.Publisher.Name)
  .ThenByDescending(book => book.Price)
  .ThenBy(book => book.Title)
  .Select(book => new { Publisher=book.Publisher.Name,
                        book.Price,
                        book.Title });

In order to get the results displayed in a web page as in figure 4.16, we use a GridView control with the markup shown in listing 4.19.

Listing 4.19. Markup used to display the results of the sorting sample (Sorting.aspx)
<asp:GridView ID="GridView1" runat="server"
  AutoGenerateColumns="false">
  <Columns>
    <asp:BoundField HeaderText="Publisher" DataField="Publisher" />
    <asp:BoundField HeaderText="Price" DataField="Price" />
    <asp:BoundField HeaderText="Book" DataField="Title" />
  </Columns>
</asp:GridView>

That’s all there is to sorting. It’s not difficult. Let’s jump to another type of operation we can use in queries.

4.5.2. Nested queries

In the previous example, the data is collected using a projection. All the information appears at the same level. We don’t see the hierarchy between a publisher and its books. Also, there is some duplication we could avoid. For example, the name of each publisher appears several times because we’ve projected this information for each book.

We’ll try to improve this by using nested queries.

Let’s look at an example to show how we can avoid projections. Let’s say we want to display publishers and their books in the same grid, as in figure 4.17.

Figure 4.17. Books grouped by publisher using nested queries

We can start by writing a query for publishers:

from publisher in SampleData.Publishers
select publisher

We said that we want both the publisher’s name and books, so instead of returning a Publisher object, we’ll use an anonymous type to group this information into an object with two properties: Publisher and Books:

from publisher in SampleData.Publishers
select new { Publisher = publisher.Name, Books = ... }

You should be used to this by now. The interesting part is: how do we get a publisher’s books? This is not a trick question.

In our sample data, books are attached to a publisher through their Publisher property. You may have noticed though that there is no backward link from a Publisher object to Book objects. Fortunately, LINQ helps us compensate for this. We can use a simple query expression, nested in the first one:

from publisher in SampleData.Publishers
select new {
  Publisher = publisher.Name,
  Books =
    from book in SampleData.Books
    where book.Publisher.Name == publisher.Name
    select book }

Listing 4.20 contains the complete source code to use in a web page.

Listing 4.20. Code-behind that demonstrates nested queries (Nested.aspx.cs)
using System;
using System.Linq;

using LinqInAction.LinqBooks.Common;

public partial class Nested : System.Web.UI.Page
{
  protected void Page_Load(object sender, EventArgs e)
  {
    GridView1.DataSource =
      from publisher in SampleData.Publishers
      orderby publisher.Name
      select new {
        Publisher = publisher.Name,
        Books =
          from book in SampleData.Books
          where book.Publisher == publisher
          select book};
    GridView1.DataBind();
  }
}

To display the Books property’s data, we’ll use an interesting feature of ASP.NET data controls: they can be nested. In listing 4.21, we use this feature to display the books in a bulleted list.

Listing 4.21. Markup for the nested queries (Nested.aspx)
<%@ Page Language="C#" AutoEventWireup="true"
  CodeFile="Nested.aspx.cs" Inherits="Nested" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Nested queries</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
      <asp:GridView ID="GridView1" runat="server"
        AutoGenerateColumns="false">
        <Columns>
          <asp:BoundField HeaderText="Publisher"
            DataField="Publisher" />
          <asp:TemplateField HeaderText="Books">
            <ItemTemplate>
              <asp:BulletedList ID="BulletedList1" runat="server"
                 DataSource='<%# Eval("Books") %>'
                 DataValueField="Title" />
            </ItemTemplate>
          </asp:TemplateField>
        </Columns>
      </asp:GridView>
    </div>
    </form>
</body>
</html>

In this markup, we use a TemplateField for the “Books” column. In this column, a BulletedList control is bound to the Books property of the anonymous type. As specified by DataValueField, it displays the Title property of each book.

In this sample, we’ve created a view on hierarchical data. This is just one kind of operation we can do with LINQ. We’ll now show you more ways to work with object graphs.

4.5.3. Grouping

In the previous sample, we showed how to create a hierarchy of data by using nested queries. We’ll now consider another way to achieve the same result using LINQ’s grouping features.

Using grouping, we’ll get the same result as with the previous sample except that we don’t see the publishers without books this time. See figure 4.18.

Figure 4.18. Books grouped by publisher using grouping

We’ll also reuse the same markup. Only the query is different. See listing 4.22.

Listing 4.22. Grouping books by publisher using a group clause (Grouping.aspx.cs)
protected void Page_Load(object sender, EventArgs e)
{
  GridView1.DataSource =
    from book in SampleData.Books
    group book by book.Publisher into publisherBooks
    select new { Publisher=publisherBooks.Key.Name,
                 Books=publisherBooks };
  GridView1.DataBind();
}

What happens here is that we ask for books grouped by publishers. All the books that belong to a specific publisher will be in the same group. In our query, such a group is named publisherBooks. The publisherBooks group is an instance of the IGrouping<TKey, T> interface. Here is how this interface is defined:

public interface IGrouping<TKey, T> : IEnumerable<T>
{
  TKey Key { get; }
}

You can see that an object that implements the IGrouping generic interface has a strongly typed key and is a strongly typed enumeration. In our case, the key is a Publisher object, and the enumeration is of type IEnumerable<Book>.

Our query returns a projection of the publisher’s name (the group’s key) and its books. This is exactly what was happening in the previous example using nested queries! This explains why we can reuse the same grid configuration for this sample.

Using the grouping operator instead of a nested query—like we did in the previous sample—offers at least two advantages. The first is that the query is shorter. The second is that we can name the group. This makes it easier to understand what the group consists of, and it allows us to reuse the group in several places within the query. For example, we could improve our query to show the books for each publisher, as well as the number of books in a separate column:

from book in SampleData.Books
group book by book.Publisher into publisherBooks
select new {
  Publisher=publisherBooks.Key.Name,
  Books=publisherBooks,
  Count=publisherBooks.Count() };

Grouping is commonly used in SQL alongside aggregation operators. Notice how we use the Count operator in a similar way in the latest code snippet. You’ll often use Count and the other aggregation operators like Sum, Min, and Max on groups.

Grouping is one way LINQ offers to deal with relationships between objects. Another is join operations.

4.5.4. Using joins

After seeing how to group data using nested queries or the grouping operator, we’ll now discover yet another way to achieve about the same result. This time, we’ll use join operators.

Join operators allow us to perform the same kind of operations as projections, nested queries, or grouping do, but their advantage is that they follow a syntax close to what SQL offers.

Group join

In order to introduce the join operators, let’s consider a query expression that uses a join clause, shown in listing 4.23.

Listing 4.23. Using a join..into clause to group books by publisher (Joins.aspx.cs)
from publisher in SampleData.Publishers
join book in SampleData.Books
  on publisher equals book.Publisher into publisherBooks
select new { Publisher=publisher.Name, Books=publisherBooks };

This is a group join. It bundles each publisher’s books as sequences named publisherBooks. This new query is equivalent to the one we wrote in section 4.5.3, which uses a group clause:

from book in SampleData.Books
group book by book.Publisher into publisherBooks
select new { Publisher=publisherBooks.Key.Name,
             Books=publisherBooks };

Look at figure 4.19 and note how the result is different than with a grouping operation. As with nested queries (see figure 4.17), publishers with no books appear in the results this time.

Figure 4.19. Group join result

After group joins, we’ll now take a look at inner joins, left outer joins, and cross joins.

Inner join

An inner join essentially finds the intersection between two sequences. With an inner join, the elements from two sequences that meet a matching condition are combined to form a single sequence.

The Join operator performs an inner join of two sequences based on matching keys extracted from the elements. For example, it can be used to display a flat view of publishers and books like the one in figure 4.20.

Figure 4.20. Inner join result

The query to use to get this result looks like listing 4.24.

This query is similar to the one we used in the group join sample. The difference here is that we don’t use the into keyword to group the elements. Instead, the books are projected on the publishers. As you can see in figure 4.20, the result sequence contains an element for each book. In our sample data, one publisher isn’t associated with any book. Note that this publisher isn’t part of the results. This is why this kind of join operation is called an inner join. Only elements from the sequences that have at least one matching element in the other sequence are kept. We’ll see in a minute how this compares with a left outer join.

Listing 4.24. Using a join clause to group books by publisher (Joins.aspx.cs)
from publisher in SampleData.Publishers
join book in SampleData.Books on publisher equals book.Publisher
select new { Publisher=publisher.Name, Book=book .Title };

Before going further, let’s take a look at listing 4.25, which shows how our last query can be written using the Join query operator.

Listing 4.25. Using the Join operator to group books by publisher

This is a case where a query expression is clearly easier to read than code based on operators. The SQL-like syntax offered by query expressions can really help avoid the complexity of some query operators.

Let’s now move on to left outer joins.

Left outer join

As we’ve just seen, with an inner join, only the combinations with elements in both joined sequences are kept. When we want to keep all elements from the outer sequence, independently of whether there is a matching element in the inner sequence, we need to perform a left outer join.

A left outer join is like an inner join, except that all the left-side elements get included at least once, even if they don’t match any right-side elements.

Let’s say for example that we want to include the publishers with no books in the results. Note in figure 4.21 how the last publisher shows up in the output even though it has no matching books.

Figure 4.21. Left outer join result

A so-called outer join can be expressed with a group join. Listing 4.26 shows the query that produces these results.

Listing 4.26. Query used to perform a left outer join (Joins.aspx.cs)
from publisher in SampleData.Publishers
join book in SampleData.Books
  on publisher equals book.Publisher into publisherBooks
from book in publisherBooks.DefaultIfEmpty()
select new {
  Publisher = publisher.Name,
  Book = book == default(Book) ? "(no books)" : book.Title
};

The DefaultIfEmpty operator supplies a default element for an empty sequence. DefaultIfEmpty uses the default keyword of generics. It returns null for reference types and zero for numeric value types. For structs, it returns each member of the struct initialized to zero or null depending on whether they are value or reference types.

In our case, the default value is null, but we can test against default(Book) to decide what to display for books.

We’ve just seen group joins, inner joins, and left outer joins. There is one more kind of join operation we’d like to introduce: cross joins.

Cross join

A cross join computes the Cartesian product of all the elements from two sequences. The result is a sequence that contains a combination of each element from the first sequence with each element from the second sequence. As a consequence, the number of elements in the result sequence is the product of the number of elements in each sequence.

Before showing you how to perform a cross join, we’d like to point out that in LINQ, it is not done with the Join operator. In LINQ terms, a cross join is a projection. It can be achieved using the SelectMany operator or by chaining from clauses in a query expression, both of which we introduced in section 4.4.2.

As an example, let’s say we want to display all the publishers and the books projected together, regardless of whether there is a link between them. We can add a column to indicate the correct association, as in figure 4.22.

Figure 4.22. Cross join result

Listing 4.27 shows the query expression that yields this result.

Listing 4.27. Query used to perform a cross join (Joins.aspx.cs)
from publisher in SampleData.Publishers
from book in SampleData.Books
select new {
  Correct = (publisher == book.Publisher),
  Publisher = publisher.Name,
  Book = book.Title };

Here is how we would do the same without a query expression, using the SelectMany and Select operators:

SampleData.Publishers.SelectMany(
  publisher => SampleData.Books.Select(
    book => new {
      Correct = (publisher == book.Publisher),
      Publisher = publisher.Name,
      Book = book.Title }));

Again, this is a case where the syntactic sugar offered by query expressions makes things easier to write and read!

After joins, we’ll discover one more way to create views on objects in memory. This time we’ll partition sequences to keep only a range of their elements.

4.5.5. Partitioning

For the moment, we’ve been displaying all the results in a single page. This is not a problem, as we don’t have long results. If we had more results to display, it could be interesting to enable some pagination mechanism.

Adding paging

Let’s say we want to display a maximum of three books on a page. This can be done easily using the GridView control’s paging features. A grid looks like with paging enabled looks like figure 4.23.

Figure 4.23. Grid with paging

The numbers at the bottom of the grid give access to the pages. Paging can be configured in the markup, as follows:

<asp:GridView ID="GridView1" runat="server"
  AllowPaging="true" PageSize="3"
  OnPageIndexChanging="GridView1_PageIndexChanging">
</asp:GridView>

The code-behind file in listing 4.28 shows how to handle paging.

Listing 4.28. Code-behind for paging in a GridView control (Paging.aspx.cs)
using System;
using System.Linq;
using System.Web.UI.WebControls;

using LinqInAction.LinqBooks.Common;

public partial class Paging : System.Web.UI.Page
{
  private void BindData()
  {
    GridView1.DataSource =
      SampleData.Books
        .Select(book => book.Title).ToList();
    GridView1.DataBind();
  }

  protected void Page_Load(object sender, EventArgs e)
  {
    if (!IsPostBack)
      BindData();
  }

  protected void GridView1_PageIndexChanging(object sender,
    GridViewPageEventArgs e)
  {
    GridView1.PageIndex = e.NewPageIndex;
    BindData();
  }
}

 

Note

Here we use ToList in order to enable paging because a sequence doesn’t provide the necessary support for it.

 

Paging is useful and easy to activate with the GridView control, but this does not have a lot to do with LINQ. The grid handles it all by itself.

We can perform the same kind of operations programmatically in LINQ queries thanks to the Skip and Take operators.

Skip and Take

When you want to keep only a range of the data returned by a sequence, you can use the two partitioning query operators: Skip and Take.

The Skip operator skips a given number of elements from a sequence and then yields the remainder of the sequence. The Take operator yields a given number of elements from a sequence and then skips the remainder of the sequence. The canonical expression for returning page index n, given pageSize is: sequence.Skip (n * pageSize).Take(pageSize).

Let’s say we want to keep only a subset of the books. We can do this thanks to two combo boxes allowing us to select the start and end indices. Figure 4.24 shows the complete list of books, as well as the filtered list:

Figure 4.24. Partitioning results

Listing 4.29 shows the code that yields these results.

Listing 4.29. Code-behind for demonstrating partitioning (Partitioning.aspx.cs)

Here’s the associated markup:

...
<body>
  <form id="form1" runat="server">
    <div>
      <h1>Complete results</h1>
      <asp:GridView ID="GridViewComplete" runat="server" />

      <h1>Partial results</h1>
      Start:
      <asp:DropDownList ID="ddlStart" runat="server"
        AutoPostBack="True" CausesValidation="True"
        OnSelectedIndexChanged="ddlStart_SelectedIndexChanged" />
      End:
      <asp:DropDownList ID="ddlEnd" runat="server"
        AutoPostBack="True" CausesValidation="True"
        OnSelectedIndexChanged="ddlStart_SelectedIndexChanged" />
      <asp:CompareValidator ID="CompareValidator1" runat="server"
        ControlToValidate="ddlStart" ControlToCompare="ddlEnd"
        ErrorMessage=
          "The second index must be higher than the first one"
        Operator="LessThanEqual" Type="Integer" /><br />
      <asp:GridView ID="GridViewPartial" runat="server" />
    </div>
  </form>
</body>
</html>

Partitioning was the last LINQ operation we wanted to show you for now. You’ve seen several query operators as well as how they can be used in practice to create views on object collections in memory. You’ll discover more operations and operators in the next chapters.

4.6. Summary

This chapter—the first on LINQ to Objects—demonstrated how to perform several kinds of operations on object collections in memory.

This chapter also introduced the LinqBooks running example. We’ll continue using it for the code samples in subsequent chapters. You also created your first ASP.NET web site and your first Windows Forms application using LINQ. Most importantly, we reviewed major standard query operators and applied typical query operations such as filtering, grouping, and sorting.

What you’ve learned in this chapter is useful for working with LINQ to Objects, but it’s important to remember that most of this knowledge also applies to all the other LINQ flavors. You’ll see how this is the case with LINQ to XML and LINQ to SQL in parts 3 and 4 of this book.

When we cover LINQ’s extensibility in chapter 12, we’ll demonstrate how to enrich the standard set of query operators with your own operators.

After learning a lot about language features and LINQ flavors in four chapters, it’s time to consider some common scenarios to help you write LINQ code. This is the subject of the next chapter, which will also cover performance considerations in order to help you avoid writing suboptimal queries.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.42.128