0

Extending Sitecore 7 search to support filtering on two lists

Recently I’ve been working making Fortis support Sitecore 7’s new search features. While implementing and testing all the different LINQ to Sitecore filtering options I found that one of my requirements wasn’t possible out of the box. I wanted to be able to take a list of ID’s and compare it to another set of ID’s to see if there were matches between the two. For example (in pseudo code) I wanted to run something like;

// Return items where MyIdListField contains at least one ID in IEnumerableListOfIds

searchContext.GetQueryable<MyTemplatePoco>().Where(x => x.MyIdListField.Contains(IEnumerableListOfIds))

// We want Sitecores LINQ to Sitecore to create a query, to be run on the search index, which is basically a bunch of OR's (or AND's) on the field;
// myidlistfield:(110d559fdea542ea9c1c8a5df7e70ef9) OR myidlistfield:(26139f2f17924cbabeaa7cfa52e9ea74 OR ... and so on

The main issue in achieving the pseudo code above (or similar anyway) is that the .Contains extension method on IEnumerable<T> doesn’t support an IEnumerable argument only a single argument of type T. Secondary to this, even if there were such an overload, Sitecore’s LINQ to Sitecore expression parser wouldn’t support it.

With the issues at hand and some time burnt I eventually came up with the following extension method(s).

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Fortis.Search
{
	public static class SearchExtensions
	{
		public static IQueryable<TSource> ContainsOr<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector, TKey values) where TKey : IEnumerable
		{
			return Contains(queryable, keySelector, values, true);
		}

		public static IQueryable<TSource> ContainsAnd<TSource, TKey>(this IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector, TKey values) where TKey : IEnumerable
		{
			return Contains(queryable, keySelector, values, false);
		}

		private static IQueryable<TSource> Contains<TSource, TKey>(IQueryable<TSource> queryable, Expression<Func<TSource, TKey>> keySelector, TKey values, bool or) where TKey : IEnumerable
		{
			// Ensure the body of the selector is a MemberExpression
			if (!(keySelector.Body is MemberExpression))
			{
				throw new InvalidOperationException("Fortis: Expression must be a member expression");
			}

			var typeofTSource = typeof(TSource);
			var typeofTKey = typeof(TKey);

			// x
			var parameter = Expression.Parameter(typeofTSource);

			// Create the enumerable of constant expressions based off of the values
			var constants = values.Cast<object>().Select(id => Expression.Constant(id));

			// Create separate MethodCallExpression objects for each constant expression created
			// type -> we need to specify the type which contains the method we want to run
			// methodName -> in this instance we need to specify the Contains method
			// typeArguments -> the type parameter from TKey
			//					e.g. if we're passing through IEnumerable<Guid> then this will pass through the Guid type
			//					this is because we're effectively running IEnumerable<Guid>.Contains(Guid guid) for each
			//					guid in our values object
			// arguments ->
			//		keySelector.Body -> this would be property we want to run the expession on e.g.
			//							IQueryable<MyPocoTemplate>.Where(x => x.MyIdListField)
			//							so keySelector.Body will contain the "x.MyIdListField" which is what we want to run
			//							each constant expression against
			//		constant -> this is the constant expression
			//
			// Each expression will effectively be like running the following;
			// x => x.MyIdListField.Contains(AnId)
			var expressions = constants.Select(constant => Expression.Call(typeof(Enumerable), "Contains", typeofTKey.GetGenericArguments(), keySelector.Body, constant));

			// Combine all the expressions into one expression so you would end with something like;
			// x => x.MyIdListField.Contains(AnId) OR x.MyIdListField.Contains(AnId) OR x.MyIdListField.Contains(AnId)
			var aggregateExpressions = expressions.Select(expression => (Expression)expression).Aggregate((x, y) => or ? Expression.OrElse(x, y) : Expression.AndAlso(x, y));

			// Create the Lambda expression which can be passed to the .Where
			var lambda = Expression.Lambda<Func<TSource, bool>>(aggregateExpressions, parameter);

			return queryable.Where(lambda);
		}
	}
}

With the extension in place you can then use it in the following manner;

// Specify the property we want to run the contains on and pass in the values

searchContext.GetQueryable<MyTemplatePoco>().ContainsOr(x => x.MyIdListField, IEnumerableListOfIds)

It’s worth noting that Sitecore 7’s ExpressionParser is lacking some functionality out of the box and I highly recommend grabbing Kam’s (@kamsar) BugFix classes which solves a number of issues :).

Jason Bert