Building Dynamic Content Search LINQ Queries In Sitecore 7

Unleash The Predicate Builder!

First things first - Sitecore 7's ContentSearch API is awesome. Words cannot describe how happy I am to be able to write LINQ queries in Sitecore 7.

Sitecore 6, you were great, but I won't miss having to write custom queries in Lucene.NET.

LINQ queries are typically fixed expressions used to find things, filter things and sort things.


var results = context.GetQueryable().Where(r =>
                    r.TemplateName == "Car" &&
                    r.Paths.Contains(new ID(Guid.NewGuid()))
                ).OrderBy(r => r.CreatedDate)
				.ToList();
					

But what if our LINQ query needs to be dynamic? With an interactive site it's common for the conditions of a LINQ query to be driven by user input. So how do we do that? I give you Sitecore's handy PredicateBuilder class.

Getting Started With The Predicate Builder

With the PredicateBuilder, we'll be using ANDs and OR to combine smaller LINQ queries into a single expression.

Each predicate is created with a default bool value used anchor our expression. The easiest way to think of this:

  • Use true with ANDs
  • Use false with ORs

Anchoring an AND to a false will never allow an expression to evaluate because false && {any expression} is false. Anchoring an OR to a true will always force an expression to evaluate because true || {any expression} is true. Make sense? If not, don't worry. Just follow 2 rules above and you'll be good.

The Code

Let's jump right into a full example. I've included my explanation in the comments.


using System.Collections.Generic;
using System.Linq;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq.Utilities;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;

namespace Fishtank.Web.Demo
{
    public class SampleQueryRunner
    {
        public List FindMakesAndYears(List makes, List years)
        {
            /*
             * Establish the query.  We're going to AND to it so we're anchoring to 'true'
             */

            var query = PredicateBuilder.True();

            // Add "makes" to our LINQ expression, if we have any
            
            if (makes != null && makes.Count != 0)
            {
                // We want to OR our 'makes' together, we anchor to 'false'

                var makesExpression = PredicateBuilder.False();
                foreach (var makeId in makes)
                {
                    makesExpression = makesExpression.Or(sri => new ID(sri["make"]) == makeId);
                }
                
                // AND our OR'd expression for 'makes' to the query
                query = query.And(makesExpression);
            }

            // Add "years" to our LINQ expression, if we have any

            if (years != null && years.Count != 0)
            {
                // years will be OR'd together, anchor to 'false'
                var yearsExpression = PredicateBuilder.False();

                // the same foreach statement used for "makes", adapted for 'years' leveled up into LINQ!
                yearsExpression = years.Aggregate(yearsExpression, (current, year) => current.Or(sri => sri["year"] == year));
                
                // AND our 'years' expression with existing queries
                query = query.And(yearsExpression);
            }
            
            /*
             * Execute your content search
             */

            var searchIndex = ContentSearchManager.GetIndex("yourIndexName");
            
            using (var context = searchIndex.CreateSearchContext())
            {
                var searchResultItems = context.GetQueryable().Where(query);
                return searchResultItems.ToList();
            }
        }
    }
}

The above code results in 3 possible queries:

// NESTED query! ids OR'd, years OR'd, AND'd together
(id1 || id2 || id3) && (year1 || year2)

// ids OR'd
(id1 || id2 || id3)

// years OR'd
(year1 || year2)

Always Return Your Expression

When building an expression, always return the expressions to a variable before adding to it.


// BAD!!! You'll have an empty expression
var expression = PredicateBuilder.False();
expression.Or(item => item["tags"] == "A");
expression.Or(item => item["tags"] == "B");
expression.Or(item => item["tags"] == "C");

// GOOD!!! 
var expression = PredicateBuilder.False();
expression = expression.Or(item => item["tags"] == "A");
expression = expression.Or(item => item["tags"] == "B");
expression = expression.Or(item => item["tags"] == "C");

Nesting Dynamic Expressions

Nest multiple-level of queries.



var expression = PredicateBuilder.False();

var filters = PredicateBuilder.True();
filters = filters.And(i => i["filter"] == "foo");
filters = filters.And(i => i["filter"] == "bar");

var tags = PredicateBuilder.False();
tags = tags.Or(i => i["tag"] == "Dan");
tags = tags.Or(i => i["tag"] == "Wes");

var categories = PredicateBuilder.True();
categories = categories.And(i => i["category"] == "Blog");
categories = categories.And(i => i["category"] == "News");

// nest 'filters' alongside 'tags'
tags = tags.Or(filters);

// nest 'tags' alongside 'categories'
categories = categories.Or(tags);

// Add all the nested queroes to to the base expression
expression = expression.Or(categories);

Leaving out the anchor values that would translate into:

(i["category"] == "Blog" &&  i["category"] == "News") || 
(
    i["tag"] == "Dan" ||  i["tag"] == "Wes"  ||
        (
            i["filter"] == "foo" && i["filter"] == "bar"
        ) 
)

Never Use False With an AND

Again, if you're ANDing expressions together don't anchor to a false.


/*
 * BAD!!! the 'tags' and 'paths' filters will never be added
 */

var expression = PredicateBuilder.False();

var tags = PredicateBuilder.False();
tags = tags.Or(item => item["tags"] == "A");
tags = tags.Or(item => item["tags"] == "B");

var paths = PredicateBuilder.False();
paths = paths.Or(item => item["state"] == "off");
paths = paths.Or(item => item["state"] == "idle");

expression = expression.And(tags);
expression = expression.And(paths);

// The expressions evaluates as:
//
// FALSE && (tags) && (paths)

Closing

I've found that the PredicateBuilder really is the key to the universe with ContentSearch API. Feel free to leave your thoughts in the comments. And as usually, this blog post was created using Markdown for Sitecore.

Fish