简体   繁体   中英

Dynamic expression tree with method 'Select'

I'm trying to build the following lambda expression using the expression tree ->

info => info.event_objects.Select(x => x.object_info.contact_info)

I researched a lot and find some answers on the StackOverflow. This one helped me to build the

info => 
     info.event_objects.Any(x => x.object_info.contact_info.someBool == true)

As you can see, the method 'Any' is easy to get.

var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" 
&& m.GetParameters().Length == 2);
anyMethod = anyMethod.MakeGenericMethod(childType);

The main problem is with the method 'Select'. If you will try to change the Name "Any" to "Select", you will get the following exception:

var selectMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == 
"Select" && m.GetParameters().Length == 2);
selectMethod = selectMethod.MakeGenericMethod(childType);

Additional information: Sequence contains more than one matching element

Another way I've tried:

MethodInfo selectMethod = null;
foreach (MethodInfo m in typeof(Enumerable).GetMethods().Where(m => m.Name 
  == "Select"))
    foreach (ParameterInfo p in m.GetParameters().Where(p => 
           p.Name.Equals("selector")))
        if (p.ParameterType.GetGenericArguments().Count() == 2)
            selectMethod = (MethodInfo)p.Member;

It seems work, but then I get the exception here:

navigationPropertyPredicate = Expression.Call(selectMethod, parameter, 
navigationPropertyPredicate);

 Additional information: Method 
 System.Collections.Generic.IEnumerable`1[TResult] Select[TSource,TResult] 
 (System.Collections.Generic.IEnumerable`1[TSource], 
 System.Func`2[TSource,TResult]) is a generic method definition> 

After that, I've tried to use:

selectMethod = selectMethod.MakeGenericMethod(typeof(event_objects), 
typeof(contact_info));

In fact, it doesn't help.

Here is my full code

 public static Expression GetNavigationPropertyExpression(Expression parameter, params string[] properties)
    {
        Expression resultExpression = null;
        Expression childParameter, navigationPropertyPredicate;
        Type childType = null;

        if (properties.Count() > 1)
        {
            //build path
            parameter = Expression.Property(parameter, properties[0]);
            var isCollection = typeof(IEnumerable).IsAssignableFrom(parameter.Type);
            //if it´s a collection we later need to use the predicate in the methodexpressioncall
            if (isCollection)
            {
                childType = parameter.Type.GetGenericArguments()[0];
                childParameter = Expression.Parameter(childType, "x");
            }
            else
            {
                childParameter = parameter;
            }
            //skip current property and get navigation property expression recursivly
            var innerProperties = properties.Skip(1).ToArray();
            navigationPropertyPredicate = GetNavigationPropertyExpression(childParameter, innerProperties);
            if (isCollection)
            {
                //var selectMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Select" && m.GetParameters().Length == 2);
                //selectMethod = selectMethod.MakeGenericMethod(childType);
                MethodInfo selectMethod = null;
                foreach (MethodInfo m in typeof(Enumerable).GetMethods().Where(m => m.Name == "Select"))
                    foreach (ParameterInfo p in m.GetParameters().Where(p => p.Name.Equals("selector")))
                        if (p.ParameterType.GetGenericArguments().Count() == 2)
                            selectMethod = (MethodInfo)p.Member;

                navigationPropertyPredicate = Expression.Call(selectMethod, parameter, navigationPropertyPredicate);
                resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
            }
            else
            {
                resultExpression = navigationPropertyPredicate;
            }
        }
        else
        {
            var childProperty = parameter.Type.GetProperty(properties[0]);
            var left = Expression.Property(parameter, childProperty);
            var right = Expression.Constant(true, typeof(bool));
            navigationPropertyPredicate = Expression.Lambda(left);
            resultExpression = MakeLambda(parameter, navigationPropertyPredicate);
        }
        return resultExpression;
    }


    private static Expression MakeLambda(Expression parameter, Expression predicate)
    {
        var resultParameterVisitor = new ParameterVisitor();
        resultParameterVisitor.Visit(parameter);
        var resultParameter = resultParameterVisitor.Parameter;
        return Expression.Lambda(predicate, (ParameterExpression)resultParameter);
    }

    private class ParameterVisitor : ExpressionVisitor
    {
        public Expression Parameter
        {
            get;
            private set;
        }
        protected override Expression VisitParameter(ParameterExpression node)
        {
            Parameter = node;
            return node;
        }
    }


    [TestMethod]
    public void TestDynamicExpression()
    {
        var parameter = Expression.Parameter(typeof(event_info), "x");
        var expression = GetNavigationPropertyExpression(parameter, "event_objects", "object_info", "contact_info");
    }

Edit: unfortunately, I've tried answers from this question, but it doesn't seem work

"Additional information: Sequence contains more than one matching element"

Unlike "Any()", for "Select()" there are two overloads with two parameters:

  1. Select<TS, TR>(IE<TS> source, Func<TS, TR> selector)
  2. Select<TS, TR>(IE<TS> source, Func<TS, int, TR> selector) (takes the "(item, index) => " selector lambda)

Since your code already relies on "esoteric knowledge" anyway, just take the first one of them:

var selectMethod = typeof(Enumerable).GetMethods()
        .First(m => m.Name == nameof(Enumerable.Select) 
                 && m.GetParameters().Length == 2);

You can avoid finding the correct generic method overload via reflection (which is complicated and error prone as you already noticed) by using one of the two Expression.Call method overloads (one for static and one for instance methods) accepting string methodName and Type[] typeArguments .

Also the current implementation is overcomplicated and contains other problems, due to the lack of clear separation of expression and lambda expression building.

Here is a correct working implementation:

public static LambdaExpression GetNavigationPropertySelector(Type type, params string[] properties)
{
    return GetNavigationPropertySelector(type, properties, 0);
}

private static LambdaExpression GetNavigationPropertySelector(Type type, string[] properties, int depth)
{
    var parameter = Expression.Parameter(type, depth == 0 ? "x" : "x" + depth);
    var body = GetNavigationPropertyExpression(parameter, properties, depth);
    return Expression.Lambda(body, parameter);
}

private static Expression GetNavigationPropertyExpression(Expression source, string[] properties, int depth)
{
    if (depth >= properties.Length)
        return source;
    var property = Expression.Property(source, properties[depth]);
    if (typeof(IEnumerable).IsAssignableFrom(property.Type))
    {
        var elementType = property.Type.GetGenericArguments()[0];
        var elementSelector = GetNavigationPropertySelector(elementType, properties, depth + 1);
        return Expression.Call(
            typeof(Enumerable), "Select", new Type[] { elementType, elementSelector.Body.Type },
            property, elementSelector);
    }
    else
    {
        return GetNavigationPropertyExpression(property, properties, depth + 1);
    }
}

The first is the public method. It internally uses the next two private methods to recursively build the desired lambda. As you can see, I distinguish between building lambda expression and just expression to be used as lambda body.

Test:

var selector = GetNavigationPropertySelector(typeof(event_info), 
    "event_objects", "object_info", "contact_info");

Result:

x => x.event_objects.Select(x1 => x1.object_info.contact_info)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM