Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Runtime creation of LINQ expression

Say I have this expression:

int setsize = 20;
Expression<Func<Foo, bool>> predicate = x => x.Seed % setsize == 1
                                          || x.Seed % setsize == 4;

This basically 'partitions' a set of elements into 20 partitions and retrieves from each set each first and fourth element.

This expression is passed to MongoDB which it's driver is perfectly capable of translating into a MongoDB "query". The predicate can, however, also be used on a list of objects (LINQ2Objects) etc. I want this expression to be reusable (DRY). However, I want to be able to pass in an IEnumerable<int> to specify which items to retrieve (so 1 and 4 aren't "hardcoded" into it):

public Expression<Func<Foo, bool>> GetPredicate(IEnumerable<int> items) {
    //Build expression here and return it
}

With LINQPad using this code:

int setsize = 20;
Expression<Func<Foo, bool>> predicate = x => x.Seed % setsize == 1 || x.Seed % setsize == 4;
predicate.Dump();

} 

class Foo
{
    public int Seed { get; set; }

I can examine the expression:

Expression

Now, I want to be able to build an exact reproduction of this expression but with a variable amount of integers to pass (so instead of 1 and 4 I could pass, for example, [1, 5, 9, 11] or [8] or [1, 2, 3, 4, 5, 6, ..., 16]).

I have tried using BinaryExpressions etc. but haven't been able to construct this message correctly. The main issue is that most of my attempts will fail when passing the predicate to MongoDB. The "hardcoded" version works fine but somehow all my attempts to pass my dynamic expressions fail to be translated into a MongoDB query by the C# driver:

{
    "$or" : [{
        "Seed" : { "$mod" : [20, 1] }
    }, {
        "Seed" : { "$mod" : [20, 4] }
    }]
}

Basically, I want to dynamically build the expression at runtime in such a way that it exactly replicates what the compiler generates for the 'hardcoded' version.

Any help will be appreciated.

EDIT

As requested in the comments (and posted on pastebin), one of my tries below. I'm posting it in the question for furure reference should pastebin take it down or stop their serivce or...

using MongoRepository;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

class Program
{
    static void Main(string[] args)
    {
        MongoRepository<Foo> repo = new MongoRepository<Foo>();
        var reporesult = repo.All().Where(IsInSet(new[] { 1, 4 }, 20)).ToArray();
    }

    private static Expression<Func<Foo, bool>> IsInSet(IEnumerable<int> seeds, int setsize)
    {
        if (seeds == null)
            throw new ArgumentNullException("s");

        if (!seeds.Any())
            throw new ArgumentException("No sets specified");

        return seeds.Select<int, Expression<Func<Foo, bool>>>(seed => x => x.Seed % setsize == seed).JoinByOr();
    }
}

public class Foo : Entity
{
    public int Seed { get; set; }
}

public static class Extensions
{
    public static Expression<Func<T, bool>> JoinByOr<T>(this IEnumerable<Expression<Func<T, bool>>> filters)
    {
        var firstFilter = filters.First();
        var body = firstFilter.Body;
        var param = firstFilter.Parameters.ToArray();
        foreach (var nextFilter in filters.Skip(1))
        {
            var nextBody = Expression.Invoke(nextFilter, param);
            body = Expression.Or(body, nextBody);
        }
        return Expression.Lambda<Func<T, bool>>(body, param);
    }
}

This results in: Unsupported where clause: <InvocationExpression>.

like image 516
RobIII Avatar asked May 16 '13 09:05

RobIII


1 Answers

Try this:

public Expression<Func<Foo, bool>> GetExpression<T>(
    int setSize, int[] elements,
    Expression<Func<Foo, T>> property)
{
    var seedProperty = GetPropertyInfo(property);
    var parameter = Expression.Parameter(typeof(Foo));
    Expression body = null;

    foreach(var element in elements)
    {
        var condition = GetCondition(parameter, seedProperty, setSize, element);
        if(body == null)
            body = condition;
        else
            body = Expression.OrElse(body, condition);
    }

    if(body == null)
        body = Expression.Constant(false);        

    return Expression.Lambda<Func<Foo, bool>>(body, parameter);    
}

public Expression GetCondition(
    ParameterExpression parameter, PropertyInfo seedProperty,
    int setSize, int element)
{
    return Expression.Equal(
        Expression.Modulo(Expression.Property(parameter, seedProperty),
                          Expression.Constant(setSize)),
        Expression.Constant(element));
}

public static PropertyInfo GetPropertyInfo(LambdaExpression propertyExpression)
{
    if (propertyExpression == null)
        throw new ArgumentNullException("propertyExpression");

    var body = propertyExpression.Body as MemberExpression;
    if (body == null)
    {
        throw new ArgumentException(
            string.Format(
                "'propertyExpression' should be a member expression, "
                + "but it is a {0}", propertyExpression.Body.GetType()));
    }

    var propertyInfo = body.Member as PropertyInfo;
    if (propertyInfo == null)
    {
        throw new ArgumentException(
            string.Format(
                "The member used in the expression should be a property, "
                + "but it is a {0}", body.Member.GetType()));
    }

    return propertyInfo;
}

You would call it like this:

GetExpression(setSize, elements, x => x.Seed);

If you want it to be generic in Foo also, you need change it like this:

public static Expression<Func<TEntity, bool>> GetExpression<TEntity, TProperty>(
    int setSize, int[] elements,
    Expression<Func<TEntity, TProperty>> property)
{
    var propertyInfo = GetPropertyInfo(property);
    var parameter = Expression.Parameter(typeof(TEntity));
    Expression body = null;

    foreach(var element in elements)
    {
        var condition = GetCondition(parameter, propertyInfo , setSize, element);
        if(body == null)
            body = condition;
        else
            body = Expression.OrElse(body, condition);
    }

    if(body == null)
        body = Expression.Constant(false);

    return Expression.Lambda<Func<TEntity, bool>>(body, parameter);    
}

Now, the call would look like this:

GetExpression(setSize, elements, (Foo x) => x.Seed);

In this scenario it is important to specify the type of x explicitly, otherwise type-inference won't work and you would have to specify both Foo and the type of the property as generic arguments to GetExpression.

like image 92
Daniel Hilgarth Avatar answered Sep 17 '22 16:09

Daniel Hilgarth