Wednesday, December 15, 2010

Using LINQ with generics and lambda expressions to add wildcard searches to a query

Adding generic search functionality as a helper class to a project has the advantage of giving greater flexibility in querying results when using linq. Using a method that returns a type of Expression<Func<T, bool>> allows the method to be used as a predicate in linq .where() clauses as per the following:

var resultSet = from p in db.Products.Where(
GenericSearch.GetWildCardCriteria<Products>("ball*","productName"))
 select p;

Given this query, the Generic type passed through is the class of the linq table that is to be queried. "ball*" is the search criteria, which would return all results starting with 'ball', using "ball*10" would give all results starting with 'ball' and ending with '10'. Alternatively, "*ball" would give all results ending with 'ball'.

Due to linq functions being relatively limited in their ability to query the database, and being much more limited that a SQL statement, the second GetWildCardCriteria<T>() method attempts to interpret a search string in terms of wildcard searches using the linq functions available, specifically, the .StartsWith(), .EndsWith() and .Contains() methods. These methods have been called through the building of the functions using the BuildExpressionFromMethod<T>() method, which is used as the search is generic, and so needs to be built dynamically so the correct type <T> and property name are used.

The BuildExpressionFromMethod<T>() can build the given .Contains(), .EndsWith(), etc. expression from the strings sent to it, and return a lambda expression to the predicate as all the functions have the same signature x.Method("comparisonText")

A direct comparison is done if there are no wildcards, and so a lambda expression for x.Field == "ball" is generated using the BuildEqualsExpression<T>() method.

Note: The PredicateBuilder extension class is also required to run this code and can be found at: http://www.albahari.com/nutshell/predicatebuilder.aspx

The code for the search is below:

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

namespace LINQ.Helpers
{
 
    public class GenericSearch
    { 
        // use this if no predicate is added to and directly using .Where()
        public static Expression<Func<T, bool>> GetWildCardCriteria<T>(string searchString, string propertyName)
        {
            return GetWildCardCriteria<T>(True<T>(), searchString, propertyName);
        }
                
        // use this method when predcate already exists - type is inferred from expression
        public static Expression<Func<T, bool>> GetWildCardCriteria<T>(Expression<Func<T, bool>> predicate, string searchString, string propertyName)
        {
            string[] parts = searchString.Split('*');
            if (parts.Length == 1)
            {
                predicate = predicate.And(BuildEqualsExpression<T>(propertyName, searchString));
            }
            else
            {
                //Check for predicate wild card
                if (parts[0] == "")
                {
                    if (parts.Length == 2)
                    {
                        //If on only 2 parts the end with
                        predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "StartsWith", parts[1]));
                    }
                    else
                    {
                        //Else do a contains
                        predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "Contains", parts[1]));
                    }
                }
                else if (parts.Length == 2 && parts[1] == "")
                {
                    //handle suffix wild card
                    predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "StartsWith", parts[0]));
                }
                else if (parts.Length == 2 && !string.IsNullOrEmpty(parts[1]))
                {
                    //handle mid string wild card
                    predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "StartsWith", parts[0]));
                    predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "EndsWith", parts[1]));
                }
                //handle internal wild cards
                for (int i = 2; i < parts.Length - 1; i++)
                {
                    predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "Contains", parts[i]));
                }
                //Check for a suffix in the more than 2 parts case
                if (parts.Length > 2 && !string.IsNullOrEmpty(parts[parts.Length - 1]))
                {
                    predicate = predicate.And(BuildExpressionFromMethod<T>(propertyName, "StartsWith", parts[parts.Length - 1]));
                }
            }
            return predicate;
        }

        // propName is name of the property, ie. if T is the linq table Product, then propName could be the field "Product_Name"
        // value is the value that propName is equal to, eg. the product 'ball' would be equivalent to x => x.Product_Name == "ball"

        private static Expression<Func<T, bool>> BuildEqualsExpression<T>(string propName, string value)
        {
            try
            {
                ParameterExpression param = Expression.Parameter(typeof(T), "x");
                MemberExpression prop = Expression.Property(param, propName);
                BinaryExpression equals = Expression.Equal(prop, Expression.Constant(value));
                Expression<Func<T, bool>> expr = Expression.Lambda<Func<T, bool>>(equals, param);

                return expr;
            }
            catch (Exception)
            {
                return x => true;
            }
        }

        // propName is same as above
        // method is the method to call on the property, eg. if method = "Contains" and prop_Name = "Product_Name",
        // and comparisonText = "ball"
        // this would be equivalent to x => x.Product_Name.Contains("ball")

        private static Expression<Func<T, bool>> BuildExpressionFromMethod<T>(string propName, string method, string comparisonText)
        {
            try
            {
                MethodInfo mi = typeof(String).GetMethod(method, new Type[] { typeof(String) });
                ParameterExpression param = Expression.Parameter(typeof(T), "x");
                MemberExpression field = Expression.PropertyOrField(param, propName);
                MethodCallExpression mce = Expression.Call(field, mi, Expression.Constant(comparisonText));
                Expression<Func<T, bool>> expr = Expression.Lambda<Func<T, bool>>(mce, param);

                return expr;
            }
            catch (Exception)
            {
                return x => true;
            }
        }

        private static Expression<Func<T, bool>> True<T>() { return f => true; }
    }
}