17

I created a filterable BindingList from this source. It works great:

list.Filter("Customer == 'Name'");

does what it should. The internals work like a parser, that converts the expression == or != into System.Linq.Expressions.Expression. In this case, == becomes System.Linq.Expressions.Expression.Equal.

Unfortunately System.Linq.Expressions.Expression does not contain a like operator and I don't know how to solve this.

The initial code looks like this:

private static Dictionary<String, Func<Expression, Expression, Expression>> 
    binaryOpFactory = new Dictionary<String, Func<Expression, Expression, Expression>>();

static Init() {
    binaryOpFactory.Add("==", Expression.Equal);
    binaryOpFactory.Add(">", Expression.GreaterThan);
    binaryOpFactory.Add("<", Expression.LessThan);
    binaryOpFactory.Add(">=", Expression.GreaterThanOrEqual);
    binaryOpFactory.Add("<=", Expression.LessThanOrEqual);
    binaryOpFactory.Add("!=", Expression.NotEqual);
    binaryOpFactory.Add("&&", Expression.And);
    binaryOpFactory.Add("||", Expression.Or);
}

Then I created an expression that will do what I want:

private static System.Linq.Expressions.Expression<Func<String, String, bool>>
    Like_Lambda = (item, search) => item.ToLower().Contains(search.ToLower());

private static Func<String, String, bool> Like = Like_Lambda.Compile();

e.g.

Console.WriteLine(like("McDonalds", "donAld")); // true
Console.WriteLine(like("McDonalds", "King"));   // false

But binaryOpFactory requires this:

Func<Expression, Expression, Expression>

The predefined expressions seem to be exactly that:

System.Linq.Expressions.Expression.Or;

Can anyone tell me how to convert my expression?

Alex Angas
  • 59,219
  • 41
  • 137
  • 210
Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189
  • And how does your LIKE operate? I can help you build an Expression, but I need to understand how you want it to work first... regex? contains? etc? – Marc Gravell Jun 05 '09 at 19:49
  • That does not matter. The final implementation will propably be with regexp. Basically I have a Func to that I pass 2 Strings and get true or false as returnvalue. My Problem is that I do not understand the Implementation of Objects in System.Linq.Expressions.Expression Namespace, which seem to be Func (look at the generic type argements of binaryOpFactory) so I cannot create my own comparison. – Jürgen Steinblock Jun 05 '09 at 20:07
  • Re comment: understanding the Expression API can take some doing... I try to cover a few basics on my blog; Jon's book (C# in Depth) also gives a high level overview. – Marc Gravell Jun 08 '09 at 12:23

2 Answers2

16

Something like:

static IEnumerable<T> WhereLike<T>(
        this IEnumerable<T> data,
        string propertyOrFieldName,
        string value)
{
    var param = Expression.Parameter(typeof(T), "x");
    var body = Expression.Call(
        typeof(Program).GetMethod("Like",
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public),
            Expression.PropertyOrField(param, propertyOrFieldName),
            Expression.Constant(value, typeof(string)));
    var lambda = Expression.Lambda<Func<T, bool>>(body, param);
    return data.Where(lambda.Compile());
}
static bool Like(string a, string b) {
    return a.Contains(b); // just for illustration
}

In terms of a Func<Expression,Expression,Expression>:

static Expression Like(Expression lhs, Expression rhs)
{
    return Expression.Call(
        typeof(Program).GetMethod("Like",
            BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
            ,lhs,rhs);
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • looks nice thx, but I need sth. that returns a Func But the question is, what is expression1, expression2 and expression3 in this context? A example how Expression.Equal works internally whould be nice. – Jürgen Steinblock Jun 05 '09 at 21:11
  • I must admit, I do not understand the whole magic behind the code, but the second pice of code works like a charm. – Jürgen Steinblock Jun 08 '09 at 06:47
  • 1
    @flem I have no idea; let me know! – Marc Gravell Sep 04 '12 at 14:22
  • 3
    @MarcGravell. It does work with LINQ-Entities but it's not ideal. The custom "Like" method is unknown and therefore cannot be translated to SQL. The result is that the query is evaluated at this point. The alterate (which allows translation to SQL) is to build the expression `p => p.Prop.Contains(val)` directly like so: `Expression op = Expression.Call(lhs, "Contains", Type.EmptyTypes, rhs);`. – Paul Fleming Sep 04 '12 at 19:23
6

I created 2 extension methods WhereFilter() for IEnumerable and IQueryable. At this way you can use this filter also with e.g. Entity Framework and is the filtering performed on the server.

I used a filter based on * (not ?) so i could use the underlaying Linq methods StartsWith(), EndsWith() and Contains(). Supported formats: A*, *A, *A*, A*B

Usage:

var filtered = list.WhereFilter(i => i.Name, "a*", "First Name");

Here the basics of the class:

/// <summary>
/// Extension Methods for Filtering on IQueryable and IEnumerable
/// </summary>
internal static class WhereFilterExtensions
{
    /// <summary>
    /// Filters a sequence of values based on a filter with asterix characters: A*, *A, *A*, A*B
    /// </summary>
    /// <param name="source"></param>
    /// <param name="selector">Field to use for filtering. (E.g: item => item.Name)</param>
    /// <param name="filter">Filter: A*, *A, *A*, A*B</param>
    /// <param name="fieldName">Optional description of filter field used in error messages</param>
    /// <returns>Filtered source</returns>
    public static IEnumerable<T> WhereFilter<T>(this IEnumerable<T> source, Func<T, string> selector, string filter, string fieldName)
    {

        if (filter == null)
            return source;

        if (selector == null)
            return source;

        int astrixCount = filter.Count(c => c.Equals('*'));
        if (astrixCount > 2)
            throw new ApplicationException(string.Format("Invalid filter used{0}. '*' can maximum occur 2 times.", fieldName == null ? "" : " for '" + fieldName + "'"));

        if (filter.Contains("?"))
            throw new ApplicationException(string.Format("Invalid filter used{0}. '?' is not supported, only '*' is supported.", fieldName == null ? "" : " for '" + fieldName + "'"));


        // *XX*
        if (astrixCount == 2 && filter.Length > 2 && filter.StartsWith("*") && filter.EndsWith("*"))
        {
            filter = filter.Replace("*", "");
            return source.Where(item => selector.Invoke(item).Contains(filter));
        }

        // *XX
        if (astrixCount == 1 && filter.Length > 1 && filter.StartsWith("*"))
        {
            filter = filter.Replace("*", "");
            return source.Where(item => selector.Invoke(item).EndsWith(filter));
        }

        // XX*
        if (astrixCount == 1 && filter.Length > 1 && filter.EndsWith("*"))
        {
            filter = filter.Replace("*", "");
            return source.Where(item => selector.Invoke(item).StartsWith(filter));
        }

        // X*X
        if (astrixCount == 1 && filter.Length > 2 && !filter.StartsWith("*") && !filter.EndsWith("*"))
        {
            string startsWith = filter.Substring(0, filter.IndexOf('*'));
            string endsWith = filter.Substring(filter.IndexOf('*') + 1);

            return source.Where(item => selector.Invoke(item).StartsWith(startsWith) && selector.Invoke(item).EndsWith(endsWith));
        }

        // XX
        if (astrixCount == 0 && filter.Length > 0)
        {
            return source.Where(item => selector.Invoke(item).Equals(filter));
        }

        // *
        if (astrixCount == 1 && filter.Length == 1)
            return source;

        // Invalid Filter
        if (astrixCount > 0)            
            throw new ApplicationException(string.Format("Invalid filter used{0}.", fieldName == null ? "" : " for '" + fieldName + "'"));

        // Empty string: all results
        return source;


    }

    /// <summary>
    /// Filters a sequence of values based on a filter with asterix characters: A*, *A, *A*, A*B
    /// </summary>
    /// <param name="source"></param>
    /// <param name="selector">Field to use for filtering. (E.g: item => item.Name)        </param>
    /// <param name="filter">Filter: A*, *A, *A*, A*B</param>
    /// <param name="fieldName">Optional description of filter field used in error messages</param>
    /// <returns>Filtered source</returns>
    public static IQueryable<T> WhereFilter<T>(this IQueryable<T> source, Expression<Func<T, string>> selector, string filter, string fieldName)
    {

        if (filter == null)
            return source;

        if (selector == null)
            return source;

        int astrixCount = filter.Count(c => c.Equals('*'));
        if (astrixCount > 2)
            throw new ApplicationException(string.Format("Invalid filter used{0}. '*' can maximum occur 2 times.", fieldName==null?"":" for '" + fieldName + "'"));

        if (filter.Contains("?"))            
            throw new ApplicationException(string.Format("Invalid filter used{0}. '?' is not supported, only '*' is supported.", fieldName == null ? "" : " for '" + fieldName + "'"));

        // *XX*
        if (astrixCount == 2 && filter.Length > 2 && filter.StartsWith("*") &&         filter.EndsWith("*"))
        {
            filter = filter.Replace("*", "");
            return source.Where(
                Expression.Lambda<Func<T, bool>>(
                    Expression.Call(selector.Body, "Contains", null,  Expression.Constant(filter)),
                    selector.Parameters[0]
                )
            );
        }

        // *XX
        if (astrixCount == 1 && filter.Length > 1 && filter.StartsWith("*"))
        {
            filter = filter.Replace("*", "");
            return source.Where(
                Expression.Lambda<Func<T, bool>>(
                    Expression.Call(selector.Body, "EndsWith", null, Expression.Constant(filter)),
                    selector.Parameters[0]
                )
            );
        }

        // XX*
        if (astrixCount == 1 && filter.Length > 1 && filter.EndsWith("*"))
        {
            filter = filter.Replace("*", "");
            return source.Where(
                Expression.Lambda<Func<T, bool>>(
                    Expression.Call(selector.Body, "StartsWith", null,         Expression.Constant(filter)),
                    selector.Parameters[0]
                )
            );
        }

        // X*X
        if (astrixCount == 1 && filter.Length > 2 && !filter.StartsWith("*") && !filter.EndsWith("*"))
        {
            string startsWith = filter.Substring(0, filter.IndexOf('*'));
            string endsWith = filter.Substring(filter.IndexOf('*') + 1);

            return source.Where(
                Expression.Lambda<Func<T, bool>>(
                    Expression.Call(selector.Body, "StartsWith", null,         Expression.Constant(startsWith)),
                    selector.Parameters[0]
                )
            ).Where(
                Expression.Lambda<Func<T, bool>>(
                    Expression.Call(selector.Body, "EndsWith", null,         Expression.Constant(endsWith)),
                    selector.Parameters[0]
                )
            );
        }

        // XX
        if (astrixCount == 0 && filter.Length > 0)
        {
            return source.Where(
                Expression.Lambda<Func<T, bool>>(
                    Expression.Equal(selector.Body, Expression.Constant(filter)),
                    selector.Parameters[0]
                )
            );
        }

        // *
        if (astrixCount == 1 && filter.Length == 1)
            return source;

        // Invalid Filter
        if (astrixCount > 0)
            throw new ApplicationException(string.Format("Invalid filter used{0}.", fieldName == null ? "" : " for '" + fieldName + "'"));

        // Empty string: all results
        return source;

    }
}
Jo VdB
  • 2,016
  • 18
  • 16
  • by 'with e.g. Entity Framework and is the filtering performed on the server' do you mean at the database - ie: translated to SQL? I was under the impression the `selector.Invoke` prevented EF from translating to SQL? – JoeBrockhaus May 14 '14 at 18:23
  • Yes, the ExpressionTree is converted by the 'LINQ Provider'. When using Enity Framework, the LINQ to Entities provider will convert it to an SQL string that will be is executed on the database. The result will be no Array but an IEnumerable that walks through the DataReader. – Jo VdB Jun 26 '14 at 15:26