JN
2 min read

Generic IQueryable 'OR' search on multiple properties using expression trees

#generics#expression-trees#linq#extension-methods#search#sql#csharp

Now available as a nuget package. Search for 'SearchExtensions' or run the following:

<p class="nuget-badge"><code>PM> Install-Package NinjaNye.SearchExtensions</code></p> **Source code can be found here: [https://github.com/ninjanye/searchextensions](https://github.com/ninjanye/searchextensions)**

Following on from my previous post on creating a generic search extension method for IQueryable, I decided to take the concept a step further and create an additional method that allows you to search multiple properties for a particular search term. The syntax I wanted to use for this new method was as follows:

//Search users where...
var ninjaUsers = dataContext.Users.Search("ninja", x => x.UserName,
                                                   x => x.FirstName,
                                                   x => x.LastName);

After a visit to stackoverflow and some expert guidance from @MarcGravell, this is the resulting code:

    public static IQueryable<T> Search<T>(this IQueryable<T> source, 
                                          string searchTerm, 
                                          params Expression<Func<T, string>>[] stringProperties)
    {
        if (String.IsNullOrEmpty(searchTerm))
        {
            return source;
        }

        var searchTermExpression = Expression.Constant(searchTerm);

        //Variable to hold merged 'OR' expression
        Expression orExpression = null;
        //Retrieve first parameter to use accross all expressions
        var singleParameter = stringProperties[0].Parameters.Single();

        //Build a contains expression for each property
        foreach (var stringProperty in stringProperties)
        {
            //Syncronise single parameter accross each property
            var swappedParamExpression = SwapExpressionVisitor.Swap(stringProperty, stringProperty.Parameters.Single(), singleParameter);

            //Build expression to represent x.[propertyX].Contains(searchTerm)
            var containsExpression = BuildContainsExpression(swappedParamExpression, searchTermExpression);

            orExpression = BuildOrExpression(orExpression, containsExpression);
        }

        var completeExpression = Expression.Lambda<Func<T, bool>>(orExpression, singleParameter);
        return source.Where(completeExpression);
    }

    private static Expression BuildOrExpression(Expression existingExpression, Expression expressionToAdd)
    {
        if (existingExpression == null)
        {
            return expressionToAdd;
        }

        //Build 'OR' expression for each property
        return Expression.OrElse(existingExpression, expressionToAdd);
    }

    private static MethodCallExpression BuildContainsExpression<T>(Expression<Func<T, string>> stringProperty, ConstantExpression searchTermExpression)
    {
        return Expression.Call(stringProperty.Body, typeof(string).GetMethod("Contains"), searchTermExpression);
    }
      
//Create SwapVisitor to merge the parameters from each property expression into one
public class SwapVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public SwapVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
    public static Expression Swap(Expression body, Expression from, Expression to)
    {
        return new SwapVisitor(from, to).Visit(body);
    }
}

Performing the following code against a DBContext (connected to a sql db):

//Search users where...
dataContext.Users.Search("ninja", x => x.UserName,  
                                          x => x.FirstName, 
                                          x => x.LastName).ToList();

Produces the following SQL:

SELECT [Extent1].[Id] AS [Id], 
       [Extent1].[UserName] AS [UserName], 
       [Extent1].[FirstName] AS [FirstName], 
       [Extent1].[LastName] AS [LastName], 
       [Extent1].[Email] AS [Email], 
FROM   [dbo].[Users] AS [Extent1]
WHERE ([Extent1].[UserName] LIKE N'%ninja%') 
   OR ([Extent1].[FirstName] LIKE N'%ninja%') 
   OR ([Extent1].[LastName] LIKE N'%ninja%')

Because I can see more extension methods being added to this code I have created a SearchExtensions project on github. Please feel free fork this and make your own additions. Many thanks to @MarcGravell for helping me to see this task through.


Leave a comment