3

I want to create dynamic lambda expressions so that I can filter a list using a set of filtering parameters. This is what I have so far:

The expression is built using the methods bellow, where T is the object type of the list

    public static Expression<Func<T, bool>> GetExpression<T>(IList<DynamicFilter> filters)
    {
        if (filters.Count == 0)
            return null;

        ParameterExpression param = Expression.Parameter(typeof(T), "t");
        Expression exp = null;

        if (filters.Count == 1)
            exp = GetExpression<T>(param, filters[0]);

        [...]

        return Expression.Lambda<Func<T, bool>>(exp, param);
    }

    private static Expression GetExpression<T>(ParameterExpression param, DynamicFilter filter)
    {
        MemberExpression member = Expression.Property(param, filter.PropertyName);
        ConstantExpression constant = Expression.Constant(filter.Value);

        [...]

        return Expression.Call(member, filterMethod, constant);
    }

I then call

List<Example> list = ...;
var deleg = ExpressionBuilder.GetExpression<Example>(dynFiltersList).Compile();
list = list.Where(deleg).ToList();

This works just as expected with an object that contains only simple types, but if there are complex types inside, the code doesn't work anymore. For example, let's say I have a member of custom type Field inside the Example class and Field has a string property Value. If filter.PropertyName would be 'Field0' (of type Field), the code would work just fine, but if I have 'Field0.Value' I would get an obvious error stating that there is no property named 'Field0.Value' inside class Example.

I tried modifying the expression building method, like this:

        MemberExpression member = null;
        if (filter.PropertyName.Contains('.'))
        {
            string[] props = filter.PropertyName.Split('.');

            ParameterExpression param1 = Expression.Parameter(typeof(T).GetProperty(props[0]).PropertyType, "t1");
            member = Expression.Property(param1, props[0]);
        }
        else
        {
            member = Expression.Property(param, filter.PropertyName);
        }

but then I got a Lambda parameter not in scope error when compiling the expression. I sort of understand why I get this error, but I don't know how to make this work.

Bottom line is I need to make the expression building method work recursively when forming the MemberExpression. I ultimately need to obtain a list = list.Where(deleg).ToList(); that translates to something like this list = list.Where(obj => obj.Field0.Value == 'something').ToList();

I've just started working with expressions so I don't really know too much in this area, but any help would be appreciated.

Thanks

Andrei Rajala
  • 33
  • 1
  • 7

4 Answers4

3

I realize this is a fairly old post, but I had the exact same problem and found something close in an answer that Mark Gravell posted here. I just modified it slightly to meet my needs and below is the result:

    private Expression GetDeepProperty(Expression parameter, string property)
    {
        var props = property.Split('.');
        var type = parameter.Type;

        var expr = parameter;
        foreach (var prop in props)
        {
            var pi = type.GetProperty(prop);
            expr = Expression.Property(expr, pi);
            type = pi.PropertyType;
        }

        return expr;
    }

Implementation:

var method = typeof (string).GetMethod("Contains", new Type[] {typeof (string)}, null);
var lambdaParameter = Expression.Parameter(typeof(TEntity), "te");
var filterExpression = Expression.Lambda<Func<TEntity, bool>> (
            filters.Select(filter => Expression.Call(GetDeepProperty(lambdaParameter, filter.Property),
                                                      method,
                                                      Expression.Constant(filter.Value))).
                Where(exp => exp != null).
                Cast<Expression>().
                ToList().
                Aggregate(Expression.Or), lambdaParameter);
Community
  • 1
  • 1
  • 1
    It looks like it could work, though I'm not able to test it right now. Anyway, I solved my problem using the dynamic linq library from Microsoft as suggested here: [Writing Dynamic Linq Queries in Linq-to-Entities](http://naspinski.net/post/Writing-Dynamic-Linq-Queries-in-Linq-to-Entities.aspx) combined with a custom expression builder. – Andrei Rajala Oct 25 '11 at 06:11
  • @AndreiRajala could you provide your solution, since the link does not work? – Cubelaster May 06 '22 at 09:06
2

I'm trying to address

so that I can filter a list using a set of filtering parameters

not by using ExpressionBuilder but instead by using a generic Filter class.

public class Filter<T> where T: class
{
    private readonly Predicate<T> criteria;

    public Filter(Predicate<T> criteria)
    {
        this.criteria = criteria;
    }

    public bool IsSatisfied(T obj)
    {
        return criteria(obj);
    }
}

First we need to have some classes.

public class Player
{
    public string Name { get; set; }
    public int Level { get; set; }
    public enum Sex { Male, Female, Other };
    public Weapon Weapon { get; set; }
}

public class Weapon
{
    public string Name { get; set; }
    public int MaxDamage { get; set; }
    public int Range { get; set; }
    public WeaponClass Class { get; set; }

    public enum WeaponClass { Sword, Club, Bow }
}

Then we need a list of objects.

var graywand = new Weapon { Name = "Graywand", MaxDamage = 42, Range = 1, Class = Weapon.WeaponClass.Sword };
var scalpel = new Weapon { Name = "Scalpel", MaxDamage = 33, Range = 1, Class = Weapon.WeaponClass.Sword };
var players = new List<Player> {
    new Player { Name = "Fafhrd", Level = 19, Weapon = graywand }, 
    new Player { Name = "Gray Mouser", Level = 19, Weapon = scalpel }, 
    new Player { Name = "Freddy", Level = 9, Weapon = graywand }, 
    new Player { Name = "Mouse", Level = 8, Weapon = scalpel} 
};

Then let's create a couple of filters and add those to a list.

var powerfulSwords = new Filter<Player>(p => p.Weapon.MaxDamage>35);
var highLevels = new Filter<Player>(p => p.Level>15);

var filters = new List<Filter<Player>>();
filters.Add(powerfulSwords);
filters.Add(highLevels);

Finally filter the list by those filters

var highLevelAndPowerfulSwords = players.Where(p => filters.All(filter => filter.IsSatisfied(p)));
var highLevelOrPowerfulSwords = players.Where(p => filters.Any(filter => filter.IsSatisfied(p)));

Only "Fafhrd" will be in highLevelAndPowerfulSwords and highLevelOrPowerfulSwords will contain all players but "Mouse".

Jonas Elfström
  • 30,834
  • 6
  • 70
  • 106
  • Sorry, but this solution will not work for me as it isn't truly dynamic. Using a Filter class like this forces me to define a lot of combinations, whereas I need to use a parametrized filter collection. I don't know at compile time what fields are filtered (simple or complex), what values I receive and what filtering function is used. That's why I need reflection and recursively built expression trees. – Andrei Rajala May 27 '11 at 07:31
  • Sorry, didn't realize you needed to construct the filters themselves at runtime. If it don't have to be very fast then it might be easier to create some Python or Ruby code and execute it with IronPython/IronRuby. – Jonas Elfström May 27 '11 at 13:56
1

Have a look at ExpressionVisitor as described here: Replacing the parameter name in the Body of an Expression

Community
  • 1
  • 1
hemp
  • 5,602
  • 29
  • 43
  • From what I see there, the solution is for linking expression trees together with AND/OR operands but that's not what I need. However, since I'm kinda new to expressions, I'm not really sure and I would appreciate some further explanations if I'm wrong. To make matters even clearer, this is the method I'm using: http://www.kos-data.com/Blog/post/Building-Where-clause-dynamically-to-filter-collection-using-LINQ-and-Expression-Trees.aspx – Andrei Rajala May 27 '11 at 07:58
0

You could generate an expression for each element in filter and combine them into one single expression using the methods below:

    public static Expression<Func<T, K>> CombineAnd<T, K>(Expression<Func<T, K>> a, Expression<Func<T, K>> b)
    {
        ParameterExpression firstParameter = a.Parameters.First();
        Expression<Func<T, K>> b1 = Expression.Lambda<Func<T, K>>(Expression.Invoke(b, firstParameter), firstParameter);
        return Expression.Lambda<Func<T, K>>(Expression.And(a.Body, b1.Body), firstParameter);
    }
    public static Expression<Func<T, K>> CombineOr<T, K>(Expression<Func<T, K>> a, Expression<Func<T, K>> b)
    {
        ParameterExpression firstParameter = a.Parameters.First();
        Expression<Func<T, K>> b1 = Expression.Lambda<Func<T, K>>(Expression.Invoke(b, firstParameter), firstParameter);
        return Expression.Lambda<Func<T, K>>(Expression.Or(a.Body, b1.Body), firstParameter);
    }
cdel
  • 717
  • 7
  • 14
  • I don't need to combine them (or at least I don't have a problem with that), I just need to obtain the full property path as I have stated in my first post. – Andrei Rajala May 27 '11 at 08:01