Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I use an array of values in a LINQ Expression builder?

I want to dynamically build a LINQ query so I can do something like

var list = n.Elements().Where(getQuery("a", "b"));

instead of

var list = n.Elements().Where(e => e.Name = new "a" || e.Name == "c");

(Most of the time, I need to pass XNames with namespaces, not just localnames...)

My problem is in accessing the array elements:

private static Func<XElement, bool> getQuery(XName[] names)
{
    var param = Expression.Parameter(typeof(XElement), "e");

    Expression exp = Expression.Constant(false);
    for (int i = 0; i < names.Length; i++)
    {
         Expression eq = Expression.Equal(
         Expression.Property(param, typeof(XElement).GetProperty("Name")!.Name),
         /*--->*/ Expression.Variable(names[i].GetType(), "names[i]")
         );
    }
    var lambda = Expression.Lambda<Func<XElement, bool>>(exp, param);

    return lambda.Compile();
}

Obviously the Variable expression is wrong, but I'm having difficulty building an expression capable of accessing the array values.

like image 894
Jay Buckman Avatar asked Dec 17 '21 20:12

Jay Buckman


2 Answers

Do you need to create an expression and compile it? Unless I'm missing some nuance to this, all you need is a function that returns a Func<XElement, bool>.

private Func<XElement, bool> GetQuery(params string[] names)
{
    return element => names.Any(n => element.Name == n);
}

This takes an array of strings and returns a Func<XElement>. That function returns true if the element name matches any of the arguments.

You can then use that as you described:

var list = n.Elements.Where(GetQuery("a", "b"));

There are plenty of ways to do something like this. For increased readability an extension like this might be better:

public static class XElementExtensions
{
    public static IEnumerable<XElement> WhereNamesMatch(
        this IEnumerable<XElement> elements, 
        params string[] names)
    {
        return elements.Where(element => 
            names.Any(n => element.Name == n));
    }
}

Then the code that uses it becomes

var list = n.Elements.WhereNamesMatch("a", "b");

That's especially helpful when we have other filters in our LINQ query. All the Where and other methods can become hard to read. But if we isolate them into their own functions with clear names then the usage is easier to read, and we can re-use the extension in different queries.

like image 141
Scott Hannen Avatar answered Oct 23 '22 03:10

Scott Hannen


If you want to write it as Expression you can do it like so:

public static Expression<Func<Person, bool>> GetQuery(Person[] names)
{
    var parameter = Expression.Parameter(typeof(Person), "e");
    var propertyInfo = typeof(Person).GetProperty("Name");

    var expression = names.Aggregate(
        (Expression)Expression.Constant(false),
        (acc, next) => Expression.MakeBinary(
            ExpressionType.Or,
            acc,
            Expression.Equal(
                Expression.Constant(propertyInfo.GetValue(next)),
                Expression.Property(parameter, propertyInfo))));
    
    return Expression.Lambda<Func<Person, bool>>(expression, parameter);
}

Whether or not you compile the expression is determined by the means you want to achieve. If you want to pass the expression to a query provider (cf. Queryable.Where) and have e.g. the database filter your values, then you may not compile the expression.

If you want to filter a collection in memory, i.e. you enumerate all elements (cf. Enumerable.Where) and apply the predicate to all the elements, then you have to compile the expression. In this case you should probably not use the Expression api as this adds complexity to your code and you are then more vulnerable to runtime errors.

like image 39
Clemens Avatar answered Oct 23 '22 04:10

Clemens