Up Create dynamic "Where" clauses

Combine predicates with PredicateBuilder

Last modified on April 26, 2011 14:27

The IdeaBlade.Linq.PredicateBuilder provides the core functionality to build dynamic LINQ Where clauses out of multiple predicates. This topic explores predicate combinations and delves further into PredicateBuilder and PredicateDescription capabilities.


Two kinds of predicate

A predicate is a function that returns true or false for an item it evaluates. A query Where clause takes a predicate that filters items to include in the query result.

Predicates come in two forms in DevForce queries.   

You use the predicate expression form when defining a LINQ query and you know the type of object the predicate will evaluate. You use the IPredicateDescription form when you don't know the type of object to filter at compile time. 

Why do we need the PredicateBuider?

We need PredicateBuilder because combining predicates of either kind is hard in .NET. Let's illustrate with two predicate expressions.

C#
Expression<Func<Customer, bool>> expr1 = (Customer c) => c.CompanyName.StartsWith("A");
Expression<Func<Customer, bool>> expr2 = (Customer c) => c.CompanyName.Contains("e");
VB
Dim expr1 As Expression(Of Func(Of Customer, Boolean)) = _
  Function(c As Customer) c.CompanyName.StartsWith("A")
Dim expr2 As Expression(Of Func(Of Customer, Boolean)) = _
  Function(c As Customer) c.CompanyName.Contains("e")

It's easy to use either of these expressions independently; it's not obvious how to combine them.

C#
var query1 = myEntityManager.Customers.Where(expr1); // ok
var query2 = myEntityManager.Customers.Where(expr2); // ok
var queryBoth = myEntityManager.Customers.Where(expr1 && expr2) // BAD - won't compile.
VB
Dim query1 = myEntityManager.Customers.Where(expr1) ' ok
Dim query2 = myEntityManager.Customers.Where(expr2) ' ok
Dim queryBoth = myEntityManager.Customers.Where(expr1 AndAlso expr2) ' BAD - won't compile.

The PredicateBuilder can combine two predicates into a third predicate; then we pass that third predicate to the query's Where clause:

C#
var expr3 = PredicateBuilder.And(expr1, expr2);
var queryBoth = myEntityManager.Customers.Where(expr3); // this works
VB
Dim expr3 = PredicateBuilder.And(expr1, expr2)
Dim queryBoth = myEntityManager.Customers.Where(expr3) ' this works

Here's a more convenient syntax that uses PredicateBuilder's predicate expression extension methods to do the same thing without mentioning PredicateBuilder explicitly:

C#
var expr3 = expr1.And(expr2);
var queryBoth = myEntityManager.Customers.Where(expr3); // this works
VB
Dim expr3 = expr1.And(expr2)
Dim queryBoth = myEntityManager.Customers.Where(expr3) ' this works

PredicateBuilder APIs

The PredicateBuilder is a static class with two kinds of static methods:

  1. methods that take predicate expression parameters and return a predicate expression - the strongly typed API.
  2. methods that take IPredicateDescription parameters and return an IPredicateDescription - the untyped API.

The two APIs mirror each other. We'll describe the strongly typed API involving predicate expressions and assume confidently that you can extrapolate to the IPredicateDescription methods.

Method Example
And PredicateBuilder.And(p1, p2, p3 .. pn)
Or PredicateBuilder.Or(p1, p2, p3 .. pn)
Not PredicateBuilder.Not(p1)
True PredicateBuilder.True<Product>()
False PredicateBuilder.False<Product>()

“p” = Predicate Expression, Expression<Func<T, bool>>, where 'T' is the type of the evaluated item. 

All combined predicates must have the same item type (e.g., Product).

The True() and False() methods return predicate expression constants that simply help you jump-start your chaining of PredicateBuilder expressions ... as seen in the examples below.

Additional overloads of Or(), And(), and Not() are extension methods that make fluent composition of predicates a little easier:
  p1.Or(p2)
  p1.And(p2)
  p1.Not()

The parallel extension methods for IPredicateDescription are located in DynamicQueryExtensions rather than PredicateBuilder.

PredicateBuilder examples

Here are some examples using the PredicateBuilder predicate expression methods:

C#
Expression<Func<Product, bool>> p1, p2, p3, p4, bigP;
// Sample predicate expressions
p1 = p => p.ProductName.Contains("Sir");
p2 = p => p.ProductName.Contains("Cajun");
p3 = p => p.ProductName.Contains("Louisiana");
p4 = p => p.UnitPrice > 20;
bigP = p1.Or(p2); // Name contains "Sir" or "Cajun"
bigP = p1.Or(p2).Or(p3); // Name contains any of the three
bigP = PredicateBuilder.Or(p1, p2, p3); // Name contains any of the 3
bigP = PredicateBuilder.Or(tests); // OR together some tests
bigP = p1.And(p4); // "Sir" and price is greater than 20
// Name contains "Cajun" and "Lousiana" and the price is greater than 20
bigP = PredicateBuilder.And(p2, p3, p4);
bigP = PredicateBuilder.And(tests); // AND together some tests
// Name contains either “Sir” or “Louisiana” AND price is greater than 20
bigP = p1.Or(p3).And(p4); //
bigP = PredicateBuilder.Not(p1); // Name does not contain "Sir"
bigP = PredicateBuilder.True<Product>().And(p1);// same as p1
bigP = PredicateBuilder.False<Product>().Or(p1);// same as p1
// Not useful
bigP = PredicateBuilder.True<Product>().Or(p1);// always true
bigP = PredicateBuilder.False<Product>().And(p1);// always false
VB
Dim p1 As Expression(Of Func(Of Product, Boolean)), p2 As Expression(Of  _
  Func(Of Product, Boolean)), p3 As Expression(Of Func(Of Product, Boolean)), _
  p4 As Expression(Of Func(Of Product, Boolean)), bigP As Expression(Of  _
  Func(Of Product, Boolean))
' Sample predicate expressions
p1 = Function(p) p.ProductName.Contains("Sir")
p2 = Function(p) p.ProductName.Contains("Cajun")
p3 = Function(p) p.ProductName.Contains("Louisiana")
p4 = Function(p) p.UnitPrice > 20
bigP = p1.Or(p2) ' Name contains "Sir" or "Cajun"
bigP = p1.Or(p2).Or(p3) ' Name contains any of the three
bigP = PredicateBuilder.Or(p1, p2, p3) ' Name contains any of the 3
bigP = PredicateBuilder.Or(tests) ' OR together some tests
bigP = p1.And(p4) ' "Sir" and price is greater than 20
' Name contains "Cajun" and "Lousiana" and the price is greater than 20
bigP = PredicateBuilder.And(p2, p3, p4)
bigP = PredicateBuilder.And(tests) ' AND together some tests
' Name contains either "Sir" or "Louisiana" AND price is greater than 20
bigP = p1.Or(p3).And(p4)
bigP = PredicateBuilder.Not(p1) ' Name does not contain "Sir"
bigP = PredicateBuilder.True(Of Product)().And(p1) ' same as p1
bigP = PredicateBuilder.False(Of Product)().Or(p1) ' same as p1
' Not useful
bigP = PredicateBuilder.True(Of Product)().Or(p1) ' always true
bigP = PredicateBuilder.False(Of Product)().And(p1) ' always false

Debugging PredicateBuilder methods

Put a breakpoint on any of the "bigP" lines and ask the debugger to show you the result as a string. Here is the Immediate Window output for 

C#
bigP = p1.Or(p3).And(p4);
VB
bigP = p1.Or(p3).And(p4)

{p => ((p.ProductName.Contains("Sir") || p.ProductName.Contains("Louisiana")) && (p.UnitPrice > Convert(20)))}

Lazy typed PredicateDescriptions

We usually specify the item type when we create a PredicateDescription. That isn't strictly necessary. We can postpone determining the query type until the PredicateDescription is used in a query. For example:

C#
var p1 = new PredicateDescription("UnitPrice", FilterOperator.IsGreaterThanOrEqualTo, 24);
var p2 = new PredicateDescription("Discontinued", FilterOperator.IsEqualTo, true);
var p3 = p1.And(p2); //unit price >= 24 and discontinued.
VB
Dim p1 = New PredicateDescription("UnitPrice", FilterOperator.IsGreaterThanOrEqualTo, 24)
Dim p2 = New PredicateDescription("Discontinued", FilterOperator.IsEqualTo, True)
Dim p3 = p1.And(p2) ' unit price >= 24 and discontinued.

The variables p1, p2, p3 are well-defined but do not have embedded query types. They can be used in any query for which they are appropriate, e.g.,

C#
var productQuery = mgr.Products.Where(p3);
var internationalProductQuery = mgr.InternationalProducts.Where(p3);
// but not
var customerQuery = mgr.Customers.Where(p3);
VB
Dim productQuery = mgr.Products.Where(p3)
Dim internationalProductQuery = mgr.InternationalProducts.Where(p3)
' but not
Dim customerQuery = mgr.Customers.Where(p3)

The customerQuery compiles but throws at runtime because Customer doesn't have a UnitPrice or Discontinued property. The productQuery and internationalQuery will run because these properties are defined for Product and InternationalProduct.

As a general rule it is best to embed the query type in the PredicateDescription when you define it unless you have a compelling reason to do otherwise. Filtering on anonymous types within a complex query is one such compelling reason.

Convert a PredicateDescription to a "predicate expression"

When a dynamic PredicateDescription has an embedded query type information, you can convert it into a strongly typed predicate expression using the ToLambdaExpression() method. Once converted, the expression is ready to use in a strongly-typed query, the kind of query you typically write in an application. The following code illustrates:

C#
// p1 was the first PredicateDescription that filtered for UnitPrice >= 24
var exprFunc = (Expression<Func<Product, bool>>)p1.ToLambdaExpression();
var filterQuery = anEntityManager.Products.Where(exprFunc);
var results = anEntityManager.ExecuteQuery(filterQuery);
VB
' p1 was the first PredicateDescription that filtered for UnitPrice >= 24
Dim exprFunc = CType(p3.ToLambdaExpression(), Expression(Of Func(_
 Of Product, Boolean)))
Dim filterQuery = _em1.Products.Where(exprFunc)
Dim results = _em1.ExecuteQuery(filterQuery)

This gambit is only possible when you can pass the PredicateDescription into compiled code that knows the target type ... as happened here. Such occasions are rare but they occur and the ToLambdaExpression() method is standing by when the opportunity to convert presents itself.

Created by DevForce on December 13, 2010 13:32

This wiki is licensed under a Creative Commons 2.0 license. XWiki Enterprise 3.2 - Documentation. Copyright © 2020 IdeaBlade