日期:2020-06-06  浏览次数:3499 次

Chapter 8 - LINQ Queries

Getting Started

Simple Filtering

string[] names = { "Tom", "Dick", "Harry" };

IEnumerable<string> filteredNames =
  System.Linq.Enumerable.Where (names, n => n.Length >= 4);
                                    
foreach (string n in filteredNames)
  Console.Write (n + "|");            // Dick|Harry|


// In LINQPad, we can also write query results using Dump:

filteredNames.Dump ("Simple use of 'Where' query operator");

Extension Methods

// "Where" is an extension method in System.Linq.Enumerable:

(new[] {"Tom", "Dick", "Harry"} ).Where (n => n.Length >= 4)
  
// (Notice that the language dropdown above is now 'C# Expression' rather than 'C# Statement').

Basic Query Expression

from n in new[] { "Tom", "Dick", "Harry" }
where n.Contains ("a")
select n
Introducing Fluent Syntax

Chaining Query Operators

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query = names
  .Where   (n => n.Contains ("a"))
  .OrderBy (n => n.Length)
  .Select  (n => n.ToUpper());
  
query.Dump();

// The same query constructed progressively:

IEnumerable<string> filtered   = names.Where      (n => n.Contains ("a"));
IEnumerable<string> sorted     = filtered.OrderBy (n => n.Length);
IEnumerable<string> finalQuery = sorted.Select    (n => n.ToUpper());

filtered.Dump   ("Filtered");
sorted.Dump     ("Sorted");
finalQuery.Dump ("FinalQuery");

Shunning Extension Methods

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query =
  Enumerable.Select (
    Enumerable.OrderBy (
      Enumerable.Where (
        names, n => n.Contains ("a")
      ), n => n.Length
    ), n => n.ToUpper()
  );
  
query.Dump ("The correct result, but an untidy query!");

Type inference

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

names.Select (n => n.Length).Dump ("Notice result is IEnumerable<Int32>; Int32 is inferred");

IEnumerable<string> sortedByLength, sortedAlphabetically;

names.OrderBy (n => n.Length)  .Dump ("Integer sorting key");
names.OrderBy (n => n)         .Dump ("String sorting key");

Natural Ordering

int[] numbers  = { 10, 9, 8, 7, 6 };

// The natural ordering of numbers is honored, making the following queries possible:

numbers.Take (3)  .Dump ("Take(3) returns the first three numbers in the sequence");
numbers.Skip (3)  .Dump ("Skip(3) returns all but the first three numbers in the sequence");
numbers.Reverse() .Dump ("Reverse does exactly as it says");

Other Operators

int[] numbers = { 10, 9, 8, 7, 6 };

"".Dump ("All of these operators are covered in more detail in Chapter 9.");

// Element operators:

numbers.First().Dump ("First");
numbers.Last().Dump ("Last");

numbers.ElementAt (1).Dump ("Second number");
numbers.OrderBy (n => n).First().Dump ("Lowest number");
numbers.OrderBy (n => n).Skip(1).First().Dump ("Second lowest number");

// Aggregation operators:

numbers.Count().Dump ("Count");
numbers.Min().Dump ("Min");

// Quantifiers:

numbers.Contains (9).Dump ("Contains (9)");
numbers.Any().Dump ("Any");
numbers.Any (n => n % 2 != 0).Dump ("Has an odd numbered element");

// Set based operators:

int[] seq1 = { 1, 2, 3 };
int[] seq2 = { 3, 4, 5 };
seq1.Concat (seq2).Dump ("Concat");
seq1.Union (seq2).Dump ("Union");
Introducing Query Expressions

A Basic Query

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

IEnumerable<string> query =
  from     n in names
  where    n.Contains ("a")   // Filter elements
  orderby  n.Length           // Sort elements
  select   n.ToUpper();       // Translate each element (project)
  
query.Dump();

A Basic Query - Translation

// With AsQueryable() added, you can see the translation to fluent syntax in the λ tab below:

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

IEnumerable<string> query =
  from      n in names
  where     n.Contains ("a")    // Filter elements
  orderby   n.Length            // Sort elements
  select    n.ToUpper();        // Translate each element (project)
  
query.Dump();

Mixing Syntax

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

(from n in names where n.Contains ("a") select n).Count()
  .Dump ("Names containing the letter 'a'");

string first = (from n in names orderby n select n).First()
  .Dump ("First name, alphabetically");

names.Where (n => n.Contains ("a")).Count()
  .Dump ("Original query, entirely in fluent syntax");
  
names.OrderBy (n => n).First()
  .Dump ("Second query, entirely in fluent syntax");

Query Syntax in its Entirety

// The following diagram fully describes query syntax.
//
// http://www.albahari.com/nutshell/linqsyntax.aspx
Deferred Execution

Introduction

var numbers = new List<int>();
numbers.Add (1);

IEnumerable<int> query = numbers.Select (n => n * 10);    // Build query

numbers.Add (2);                    // Sneak in an extra element

query.Dump ("Notice both elements are returned in the result set");

Reevaluation

var numbers = new List<int>() { 1, 2 };

IEnumerable<int> query = numbers.Select (n => n * 10);

query.Dump ("Both elements are returned");

numbers.Clear();

query.Dump ("All the elements are now gone!");

Defeating Reevaluation

var numbers = new List<int>() { 1, 2 };

List<int> timesTen = numbers
  .Select (n => n * 10) 
  .ToList();                      // Executes immediately into a List<int>

numbers.Clear();
timesTen.Count.Dump ("Still two elements present");

Captured Variables

int[] numbers = { 1, 2 };

int factor = 10;
IEnumerable<int> query = numbers.Select (n => n * factor);

factor = 20;

query.Dump ("Notice both numbers are multiplied by 20, not 10");

Captured Variables in a for-loop

// Suppose we want to build up a query that strips all the vowels from a string.
// The following (although inefficient) gives the correct result:

IEnumerable<char> query = "Not what you might expect";

query = query.Where (c => c != 'a');
query = query.Where (c => c != 'e');
query = query.Where (c => c != 'i');
query = query.Where (c => c != 'o');
query = query.Where (c => c != 'u');

new string (query.ToArray()).Dump ("All vowels are stripped, as you'd expect.");

"Now, let's refactor this. First, with a for-loop:".Dump();

string vowels = "aeiou";

for (int i = 0; i < vowels.Length; i++)
  query = query.Where (c => c != vowels[i]);   // IndexOutOfRangeException

foreach (char c in query) Console.Write (c);

// An IndexOutOfRangeException is thrown! This is because, as we saw in Chapter 4 
// (see "Capturing Outer Variables"), the compiler scopes the iteration variable 
// in the for loop as if it was declared outside the loop. Hence each closure captures
// the same variable (i) whose value is 5 when the query is enumerated.

Solution

// We can make the preceding query work correctly by assigning the loop variable to another
// variable declared inside the statement block:

IEnumerable<char> query = "Not what you might expect";
string vowels = "aeiou";

for (int i = 0; i < vowels.Length; i++)
{
  char vowel = vowels[i];
  query = query.Where (c => c != vowel);
}

foreach (char c in query) Console.Write (c);

Captured Variables and foreach

// Let's now see what happens when you capture the iteration variable of a foreach loop:

IEnumerable<char> query = "Not what you might expect";
string vowels = "aeiou";

foreach (char vowel in vowels)
  query = query.Where (c => c != vowel);

foreach (char c in query) Console.Write (c);

// The output depends on which version of C# you're running! In C# 4.0 and C# 3.0, we
// get the same problem we had with the for-loop: each loop iteration captures the same
// variable, whose final value is 'u'. Hence only the 'u' is stripped. The workaround
// for this is to use a temporary variable (see next example).

// From C# 5.0, they fixed the compiler so that the iteration variable of a foreach loop
// is treated as *local* to each loop iteration. Hence our example strips all vowels
// as expected.
Subqueries

Basic Subquery

string[] musos = { "Roger Waters", "David Gilmour", "Rick Wright", "Nick Mason"  };

musos.OrderBy (m => m.Split().Last())   .Dump ("Sorted by last name");

Reformulating the Subquery

// For more information on subqueries, see Chapter 9, "Projecting"

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

names.Where (n => n.Length == names.OrderBy (n2 => n2.Length)
     .Select (n2 => n2.Length).First())
     .Dump();

var query =
  from   n in names
  where  n.Length == (from n2 in names orderby n2.Length select n2.Length).First()
  select n;

query.Dump ("Same thing as a query expression");

query =
  from   n in names
  where  n.Length == names.OrderBy (n2 => n2.Length).First().Length
  select n;

query.Dump ("Reformulated");

query =
  from   n in names
  where  n.Length == names.Min (n2 => n2.Length)
  select n;

query.Dump ("Same result, using Min aggregation");

Avoiding Subqueries

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

int shortest = names.Min (n => n.Length);
(
  from    n in names
  where   n.Length == shortest
  select  n
)
.Dump ("No subquery");
Composition Strategies

Progressive Query Building

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

(
  names
  .Select  (n => n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", ""))
  .Where   (n => n.Length > 2)
  .OrderBy (n => n)
)
.Dump ("This query was written in fluent syntax");

(
  from    n in names
  where   n.Length > 2
  orderby n
  select  n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
)
.Dump ("An incorrect translation to query syntax");

IEnumerable<string> query =
  from   n in names
  select n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "");

query = from n in query where n.Length > 2 orderby n select n;

query.Dump ("A correct translation to query syntax, querying in two steps");

The into Keyword

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

(
  from   n in names
  select n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
  into   noVowel 
  where  noVowel.Length > 2 orderby noVowel select noVowel
)
.Dump ("The preceding query revisited, with the 'into' keyword");

into keyword - Scoping Rules

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };

// The following will not compile - "The name 'n1' does not exist in the current context" (try it).

var query =
  from   n1 in names
  select n1.ToUpper()
  into   n2                          // Only n2 is visible from here on.
  where  n1.Contains ("x")           // Illegal: n1 is not in scope.
  select n2;
    
// The equivalent in fluent syntax (you wouldn't expect this to compile!):

var query = names
  .Select (n1 => n1.ToUpper())
  .Where (n2 => n1.Contains ("x"));     // Error: n1 no longer in scope

Wrapping Queries

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

IEnumerable<string> query =
  from n1 in 
  (
    from   n2 in names
    select n2.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
  )
  where n1.Length > 2 orderby n1 select n1;
  
query.Dump ("Here, one query wraps another");

var sameQuery = names
  .Select  (n => n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", ""))
  .Where   (n => n.Length > 2)
  .OrderBy (n => n);
  
sameQuery.Dump ("In fluent syntax, such queries translate to a linear chain of query operators");
Projection Strategies

Object Initializers

void Main()
{
  var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

  IEnumerable<TempProjectionItem> temp =
    from n in names
    select new TempProjectionItem
    {
      Original  = n,
      Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
    };
  
  temp.Dump();
}

class TempProjectionItem
{
  public string Original;      // Original name
  public string Vowelless;     // Vowel-stripped name
}

Anonymous Types

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();

var intermediate = from n in names
select new
{
  Original = n,
  Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
};

(
  from    item in intermediate
  where   item.Vowelless.Length > 2
  select  item.Original
)
.Dump();

// With the into keyword we can do this in one step:

(
  from n in names
  select new
  {
    Original = n,
    Vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
  }
  into   temp
  where  temp.Vowelless.Length > 2
  select temp.Original
)
.Dump ("With the 'into' keyword");

The let Keyword

var names = new[] { "Tom", "Dick", "Harry", "Mary", "Jay" }.AsQueryable();
(
  from n in names
  let vowelless = n.Replace ("a", "").Replace ("e", "").Replace ("i", "").Replace ("o", "").Replace ("u", "")
  where vowelless.Length > 2
  orderby vowelless
  select n           // Thanks to let, n is still in scope.
)
.Dump();

More Info

// Refer to Chapter 9 - LINQ Query Operators
Interpreted Queries

Simple EFCore Query

from    c in Customers
where   c.Name.Contains ("a")
orderby c.Name.Length
select  c.Name.ToUpper()

Combining Interpreted and Local Queries

void Main()
{
  // This uses a custom 'Pair' extension method, defined below.
  
  Customers
    .Select (c => c.Name.ToUpper())
    .Pair()                  // Local from this point on.
    .OrderBy (n => n)
    .Dump();  

  // Here's a more substantial example:

  Customers
    .Select (c => c.Name.ToUpper())
    .OrderBy (n => n)
    .Pair()                         // Local from this point on.
    .Select ((n, i) => "Pair " + i.ToString() + " = " + n)
    .Dump();  
}

public static class MyExtensions
{
  public static IEnumerable<string> Pair (this IEnumerable<string> source)
  {
    string firstHalf = null;
    foreach (string element in source)
    if (firstHalf == null)
      firstHalf = element;
    else
    {
      yield return firstHalf + ", " + element;
      firstHalf = null;
    }
  }
}

Regex in database query

Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");

// The following query throws an exception, because Regex has no equivalent in SQL:

var query = MedicalArticles
  .Where (article => article.Topic == "influenza"
      && wordCounter.Matches (article.Abstract).Count < 100);

query.Dump();

AsEnumerable

Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");

// Click the 'SQL' tab below after running this query - notice that only the topic filtering
// predicate executes on SQL Server.

var query = MedicalArticles
  .Where (article => article.Topic == "influenza")
  .AsEnumerable()
  .Where (article => wordCounter.Matches (article.Abstract).Count < 100);

query.Dump();
EF Core

Introducing DbContext

// In LINQPad we're *inside* the typed DbContext, so don't need to instantiate it:

Customers                          .Dump ("All customers");
Customers.Count()                  .Dump ("Number of rows in customer table");
Customers.Single (c => c.ID == 2)  .Dump ("Customer with ID of 2");

Using DbContext

var dbContext = this;
Console.WriteLine (dbContext.Customers.Count());

// Insert a new customer
Customer cust = new Customer()
{
  ID = 10,
  Name = "Sara Wells"
};

dbContext.Customers.Add (cust);
dbContext.SaveChanges();    // Writes changes back to database

// Query the database for the customer that was just inserted:
using (var anotherContext = new TypedDataContext (this.Database.GetDbConnection().ConnectionString))
  anotherContext.Customers
    .Single (c => c.Name == "Sara Wells")
    .Dump ("Retrieved from database");

// Update the customer's name, and save the changes to the database:
cust.Name = "Dr. Sara Wells";
dbContext.SaveChanges();

// Delete the customer
Customers.Remove (cust);
dbContext.SaveChanges();

// Click the "SQL" tab to see the SQL commands;

Object Tracking

Customer a = Customers.Where (c => c.Name.StartsWith ("T")).First();
Customer b = Customers.OrderBy (c => c.ID).First();

a.Dump();
b.Dump();

(a == b).Dump();

Change Tracking

var dbContext = this;

dbContext.Add (new Customer() { Name = "Dylan" });
ShowChanges (dbContext, "Added to context");
// Added to context
// EfCoreLib.Customer is Added
//     ID: '-2147482643' modified: False
//     Name: 'Dylan' modified: False

dbContext.SaveChanges();
ShowChanges (dbContext, "Added customer was saved.");
// Added customer was saved.
// EfCoreLib.Customer is Unchanged
//     ID: '10' modified: False
//     Name: 'Dylan' modified: False

var dylan = dbContext.Customers.First (c => c.Name == "Dylan");

ShowChanges (dbContext, "Customer loaded");
// Customer loaded
// EfCoreLib.Customer is Unchanged
//     ID: '10' modified: False
//     Name: 'Dylan' modified: False

dylan.Name = "Dylan Modified";
ShowChanges (dbContext, "Modified Name property");
// Modified Name property
// EfCoreLib.Customer is Modified
//     ID: '10' modified: False
//     Name: 'Dylan Modified' modified: True

dbContext.SaveChanges();
dbContext.Customers.Remove (dylan);
ShowChanges (dbContext, "Removed from context");
// Removed from context
// EfCoreLib.Customer is Deleted
//     ID: '10' modified: False
//     Name: 'Dylan Modified' modified: False

dbContext.SaveChanges();
ShowChanges (dbContext, "Saved to DB");
// Saved to DB
// (No changes to show)


void ShowChanges (DbContext dbContext, string title)
{
  Console.WriteLine (title);
  foreach (var e in dbContext.ChangeTracker.Entries())
  {
    Console.WriteLine ($"{e.Entity.GetType().FullName} is {e.State}");
    foreach (var m in e.Members)
      Console.WriteLine (
        $"  {m.Metadata.Name}: '{m.CurrentValue}' modified: {m.IsModified}");
  }
}

Navigation Properties - Querying

Customers.Where (c => c.Purchases.Any())

Navigation Properties - Updating

// Retrieve a customer:
Customer cust = Customers.Single (c => c.ID == 1);

// Create two purchases:
Purchase p1 = new Purchase { ID = 100, Description="Bike",  Price=500, Date = DateTime.Now };
Purchase p2 = new Purchase { ID = 101, Description="Tools", Price=100, Date = DateTime.Now };

// and add them to the customer's Purchases colleciton.
cust.Purchases.Add (p1);
cust.Purchases.Add (p2);

SaveChanges();

Purchases.RemoveRange (p1, p2);
SaveChanges();

Loading Navigation Properties

void Main()
{
  // LINQPad enables lazy loading automatically, so to demonstrate life without
  // lazy loading, we've created our own typed DbContext class below:  
  using var dbContext = new NutshellContext();
  
  var cust = dbContext.Customers.First();
  Console.WriteLine (cust.Purchases?.Count ?? 0);    // Always 0

  cust = dbContext.Customers
    .Include (c => c.Purchases)
    .Where (c => c.ID == 2)
    .First()
    .Dump ("Using Include");

  var custInfo = dbContext.Customers
   .Where (c => c.ID == 2)
   .Select (c => new
   {
     Name = c.Name,
     Purchases = c.Purchases.Select (p => new { p.Description, p.Price })
   })
   .First()
   .Dump ("Using a projection");
   
  // Yet another solution:
  dbContext.Entry (cust).Collection (b => b.Purchases).Load();
  // cust.Purchases is now populated.
}

public class NutshellContext : DbContext
{
  public DbSet<Customer> Customers { get; set; }
  public DbSet<Purchase> Purchases { get; set; }

  protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseSqlServer (Util.CurrentCxString);
  }

  protected override void OnModelCreating (ModelBuilder modelBuilder)
  {
    modelBuilder.Entity<Customer> (entity =>
     {
       entity.ToTable ("Customer");
       entity.Property (e => e.Name).IsRequired();  // Column is not nullable
     });
    modelBuilder.Entity<Purchase> (entity =>
     {
       entity.ToTable ("Purchase");
       entity.Property (e => e.Date).IsRequired();
       entity.Property (e => e.Description).IsRequired();
     });
  }
}

Extra - Compiled Queries

// EF Core lets you precompile queries so that you pay the cost of translating
// the query from LINQ into SQL only once. In LINQPad the typed DataContext is
// called TypeDataContext, so we proceed as follows:

var cc = EF.CompileQuery ((TypedDataContext dc, decimal minPrice) =>    
  from c in Customers
  where c.Purchases.Any (p => p.Price > minPrice)
  select c
);

cc (this, 100).Dump ("Customers who spend more than $100");
cc (this, 1000).Dump ("Customers who spend more than $1000");

Deferred Execution

var query = from c in Customers
            select
               from p in c.Purchases
               select new { c.Name, p.Price };

foreach (var customerPurchaseResults in query)
  foreach (var namePrice in customerPurchaseResults)
    Console.WriteLine ($"{ namePrice.Name} spent { namePrice.Price}");

var query2 = from c in Customers
             select new { c.Name, c.Purchases };

foreach (var row in query2)
  foreach (Purchase p in row.Purchases)   // No extra round-tripping
    Console.WriteLine (row.Name + " spent " + p.Price);

Deferred Execution - round-tripping

foreach (Customer c in Customers.ToArray())
  foreach (Purchase p in c.Purchases)    // Another SQL round-trip
    Console.WriteLine (c.Name + " spent " + p.Price);

// Take a look at the SQL translation tab below to see all the round-tripping.
Building Query Expressions

Compiling Expression Trees

Products.RemoveRange (Products.Where (p => p.ID == 999));
Products.Add (new Product { ID = 999, Description = "Test", LastSale = DateTime.Now } );
SaveChanges();

Product[] localProducts = Products.ToArray();

Expression<Func<Product, bool>> isSelling = 
  p => !p.Discontinued && p.LastSale > DateTime.Now.AddDays (-30);

IQueryable<Product> sqlQuery = Products.Where (isSelling);
IEnumerable<Product> localQuery = localProducts.Where (isSelling.Compile());

sqlQuery.Dump ("SQL Query");
localQuery.Dump ("Local Query, using same predicate");

AsQueryable

void Main()
{
  FilterSortProducts (Products).Dump ("This query executes on SQL Server");
  
  Product[] localProducts =
  {
    new Product { ID = 1, Description = "Local Product Test", LastSale = new DateTime (2007, 2, 3) }
  };
  
  FilterSortProducts (localProducts.AsQueryable()).Dump ("The same query - executing locally");
}

IQueryable<Product> FilterSortProducts (IQueryable<Product> input)
{
  return 
    from p in input
    where !p.Discontinued && p.LastSale < DateTime.Now.AddDays (-7)
    orderby p.Description
    select p;
}

Examining an Expression Tree

Expression<Func<string, bool>> f = s => s.Length < 5;

f.Body.NodeType.Dump ("Body.NodeType");
(((BinaryExpression) f.Body).Right).Dump ("Body.Right");

f.Dump ("The whole expression tree");

Building an Expression Tree

ParameterExpression p = Expression.Parameter (typeof (string), "s");

MemberExpression stringLength = Expression.Property (p, "Length");
ConstantExpression five = Expression.Constant (5);

BinaryExpression comparison = Expression.LessThan (stringLength, five);

Expression<Func<string, bool>> lambda = Expression.Lambda<Func<string, bool>> (comparison, p);

Func<string, bool> runnable = lambda.Compile();

runnable ("kangaroo")  .Dump ("kangaroo is less than 5 characters");
runnable ("dog")       .Dump ("dog is less than 5 characters");

Extra - Using PredicateBuilder

// Refer to http://www.albahari.com/expressions/ for info on PredicateBuilder.
//
// Note: PredicateBuilder is built into LINQPad. 
//       To enable, press F4 (for query properties) and go to the 'Advanced' tab.

string[] keywords = { "Widget", "Foo", "Bar" };

var predicate = PredicateBuilder.False<Product>();

foreach (string keyword in keywords)
{
  string temp = keyword;
  predicate = predicate.Or (p => p.Description.Contains (temp));
}

var compiledPredicate = predicate.Compile();

Products.Where (compiledPredicate).Dump ("Notice the multiple OR clauses in the SQL pane");

Extra - Dynamic Ordering Sample

// Thanks to Matt Warren, of the Microsoft LINQ to SQL team, for illustrating how this is done.
//
// Suppose you want order a query based on string that you receive at runtime. The string
// indicates a property or field name, such as "Price" or "Description" or "Date".

// For this, you need to dynamically contruct an "OrderBy" MethodCallExpression. This, in turn,
// requires a dynamically constructed LambdaExpression that references the property or field
// upon which to sort. Here's the complete solution:

IQueryable query =            // The original unordered query
  from p in Purchases
  where p.Price > 100
  select p;

string propToOrderBy = "Price";     // Try changing this to "Description" or "Date"

ParameterExpression purchaseParam = Expression.Parameter (typeof (Purchase), "p");
MemberExpression member = Expression.PropertyOrField (purchaseParam, propToOrderBy);
LambdaExpression lambda = Expression.Lambda (member, purchaseParam);

Type[] exprArgTypes = { query.ElementType, lambda.Body.Type };

MethodCallExpression methodCall =
  Expression.Call (typeof (Queryable), "OrderBy", exprArgTypes, query.Expression, lambda);

IQueryable orderedQuery = query.Provider.CreateQuery (methodCall);
orderedQuery.Dump();

Extra - Dynamic Ordering - How it Works

// Let's break down the last example. We started with a simple unordered query (remember
// that the query does not evaluate at this point, thanks to deferred execution):

IQueryable<Purchase> query =            // The original unordered query
  from p in Purchases
  where p.Price > 100
  select p;

// Here's the property or field name upon which we want to order:

string propToOrderBy = "Price";     // Try changing this to "Description" or "Date"

// The aim is to dynamically constuct the following:
// var orderedQuery = query.OrderBy (p => p.Price);

// Starting from the inside out, we start by creating the lambda expression, p => p.Price.
// To dynamically build a LambaExpression, we first create the parameter, in this case, p.
// Our parameter is of type Purchase, and is called "p":

ParameterExpression purchaseParam = Expression.Parameter (typeof (Purchase), "p");
purchaseParam.Dump ("purchaseParam");

// Next, we need to create "p.Price". The static method Expression.PropertyOrField returns
// a MemberExpression that finds a property or field with the given name:

MemberExpression member = Expression.PropertyOrField (purchaseParam, propToOrderBy);
member.Dump ("member");

// With these two things, we build the LambdaExpression:

LambdaExpression lambda = Expression.Lambda (member, purchaseParam);
lambda.Dump ("lambda");
lambda.ToString().Dump ("lambda.ToString");

// We now need to wrap the lambda expression in a MethodCallExpression that
// references the Queryable.OrderBy method. For this, we call the static Expresion.Call
// method, which is overloaded especially to simplify the task of invoking methods
// that accept lambda expressions:

Type[] exprArgTypes = { query.ElementType, lambda.Body.Type };

MethodCallExpression methodCall =
  Expression.Call (
    typeof (Queryable),   // Type defining method we want to call
    "OrderBy",            // Name of method to call
    exprArgTypes,         // Generic argument types
    query.Expression,     // First argument (the query expression body) 
    lambda);              // Second argument (the lambda expression)

methodCall.Dump ("methodCall (notice all the work that Expression.Call does for us)");

// The final step is to create the new query, which calls the expression we've just
// created. For this, we use the Provider property exposed by the IQueryable interface,
// which returns an object upon which we call CreateQuery:

IQueryable orderedQuery = query.Provider.CreateQuery (methodCall);

// (Exactly the same thing happens when you ordinarily call Queryable.OrderBy;
// you can see this by decompiling with ILSpy). 

// Here's the final result: 

orderedQuery.Expression.ToString().Dump ("The final result");