Как создать дерево выражений, вызывающее IEnumerable ‹TSource› .Any ()?

Я пытаюсь создать дерево выражений, которое представляет следующее:

myObject.childObjectCollection.Any(i => i.Name == "name");

В сокращенном виде для наглядности у меня есть следующее:

//'myObject.childObjectCollection' is represented here by 'propertyExp'
//'i => i.Name == "name"' is represented here by 'predicateExp'
//but I am struggling with the Any() method reference - if I make the parent method
//non-generic Expression.Call() fails but, as per below, if i use <T> the 
//MethodInfo object is always null - I can't get a reference to it

private static MethodCallExpression GetAnyExpression<T>(MemberExpression propertyExp, Expression predicateExp)
{
    MethodInfo method = typeof(Enumerable).GetMethod("Any", new[]{ typeof(Func<IEnumerable<T>, Boolean>)});
    return Expression.Call(propertyExp, method, predicateExp);
}

Что я делаю неправильно? У кого-нибудь есть предложения?


person flesh    schedule 28.11.2008    source источник
comment
Суть проблемы: http://stackoverflow.com/questions/269578/get-a-generic-method-without-using-getmethods   -  person nawfal    schedule 08.10.2013


Ответы (2)


Есть несколько ошибок в том, как вы это делаете.

  1. Вы смешиваете уровни абстракции. Параметр T для GetAnyExpression<T> может отличаться от параметра типа, используемого для создания экземпляра propertyExp.Type. Параметр типа T в стеке абстракции на один шаг ближе ко времени компиляции - если вы не вызываете GetAnyExpression<T> через отражение, он будет определен во время компиляции, но тип, встроенный в выражение, переданное как propertyExp, определяется во время выполнения. Передача вами предиката как Expression также является путаницей с абстракциями, и это следующий момент.

  2. Предикат, который вы передаете GetAnyExpression, должен быть значением делегата, а не Expression какого-либо вида, поскольку вы пытаетесь вызвать Enumerable.Any<T>. Если вы пытались вызвать версию Any в виде дерева выражений, тогда вам следует вместо этого передать LambdaExpression, который вы бы цитировали, и это один из редких случаев, когда вам может быть оправдано передать более конкретный тип, чем Expression, что подводит меня к следующему пункту.

  3. В общем, вы должны передавать Expression значений. При работе с деревьями выражений в целом - а это применимо ко всем типам компиляторов, а не только к LINQ и его друзьям - вы должны делать это таким образом, чтобы не зависеть от непосредственного состава дерева узлов, с которым вы работаете. Вы предполагаете, что звоните Any по MemberExpression, но на самом деле вам не нужно знать, что вы имеете дело с MemberExpression, просто Expression типа некоторая реализация IEnumerable<>. Это частая ошибка людей, не знакомых с основами AST компилятора. Франс Баума неоднократно совершал ту же ошибку, когда впервые начал работать с деревьями выражений - размышляя в особых случаях. Думайте в целом. Вы избавите себя от лишних хлопот в среднесрочной и долгосрочной перспективе.

  4. И вот суть вашей проблемы (хотя вторая и, вероятно, первая проблема вас укусила бы, если бы вы ее преодолели) - вам нужно найти соответствующую общую перегрузку метода Any, а затем создать ее экземпляр с правильным типом. Отражение здесь не дает легкого; вам нужно перебрать и найти подходящую версию.

Итак, разберемся: вам нужно найти общий метод (Any). Вот служебная функция, которая это делает:

static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, 
    Type[] argTypes, BindingFlags flags)
{
    int typeArity = typeArgs.Length;
    var methods = type.GetMethods()
        .Where(m => m.Name == name)
        .Where(m => m.GetGenericArguments().Length == typeArity)
        .Select(m => m.MakeGenericMethod(typeArgs));

    return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}

Однако для этого требуются аргументы типа и правильные типы аргументов. Получение этого из вашего propertyExp Expression не совсем тривиально, потому что Expression может быть типа List<T> или другого типа, но нам нужно найти экземпляр IEnumerable<T> и получить его аргумент типа. Я заключил это в пару функций:

static bool IsIEnumerable(Type type)
{
    return type.IsGenericType
        && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}

static Type GetIEnumerableImpl(Type type)
{
    // Get IEnumerable implementation. Either type is IEnumerable<T> for some T, 
    // or it implements IEnumerable<T> for some T. We need to find the interface.
    if (IsIEnumerable(type))
        return type;
    Type[] t = type.FindInterfaces((m, o) => IsIEnumerable(m), null);
    Debug.Assert(t.Length == 1);
    return t[0];
}

Итак, для любого Type, теперь мы можем извлечь IEnumerable<T> экземпляр из него - и утверждать, нет ли (точно) одного.

С этой работой решить настоящую проблему не так уж сложно. Я переименовал ваш метод в CallAny и изменил типы параметров, как было предложено:

static Expression CallAny(Expression collection, Delegate predicate)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType);

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
            collection,
            Expression.Constant(predicate));
}

Вот процедура Main(), которая использует весь приведенный выше код и проверяет, работает ли он в тривиальном случае:

static void Main()
{
    // sample
    List<string> strings = new List<string> { "foo", "bar", "baz" };

    // Trivial predicate: x => x.StartsWith("b")
    ParameterExpression p = Expression.Parameter(typeof(string), "item");
    Delegate predicate = Expression.Lambda(
        Expression.Call(
            p,
            typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
            Expression.Constant("b")),
        p).Compile();

    Expression anyCall = CallAny(
        Expression.Constant(strings),
        predicate);

    // now test it.
    Func<bool> a = (Func<bool>) Expression.Lambda(anyCall).Compile();
    Console.WriteLine("Found? {0}", a());
    Console.ReadLine();
}
person Barry Kelly    schedule 28.11.2008
comment
Барри - я очень ценю, что вы нашли время, чтобы объяснить мне все это, большое спасибо, я постараюсь это сделать на выходных :) - person flesh; 29.11.2008

Ответ Барри дает рабочее решение вопроса, поставленного на оригинальном плакате. Спасибо обоим этим людям за вопросы и ответы.

Я нашел этот поток, когда пытался придумать решение очень похожей проблемы: программное создание дерева выражений, которое включает вызов метода Any (). Однако в качестве дополнительного ограничения конечной целью моего решения было передать такое динамически созданное выражение через Linq-to-SQL, чтобы работа оценки Any () фактически выполнялась в Сама БД.

К сожалению, решение, которое обсуждалось до сих пор, не подходит для Linq-to-SQL.

Исходя из предположения, что это может быть довольно популярной причиной для построения динамического дерева выражений, я решил дополнить поток своими выводами.

Когда я попытался использовать результат Barry CallAny () в качестве выражения в предложении Linq-to-SQL Where (), я получил InvalidOperationException со следующими свойствами:

  • HResult = -2146233079
  • Message = "Внутренняя ошибка поставщика данных .NET Framework 1025"
  • Источник = System.Data.Entity

После сравнения жестко запрограммированного дерева выражений с динамически созданным с помощью CallAny () я обнаружил, что основная проблема связана с Compile () выражения предиката и попыткой вызвать полученный делегат в CallAny (). Не углубляясь в детали реализации Linq-to-SQL, мне казалось разумным, что Linq-to-SQL не знает, что делать с такой структурой.

Поэтому после некоторых экспериментов я смог достичь желаемой цели, немного изменив предложенную реализацию CallAny (), чтобы она принимала выражение predicateExpression, а не делегат для логики предиката Any ().

Мой исправленный метод:

static Expression CallAny(Expression collection, Expression predicateExpression)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType); // (see "NOTE" below)

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
        collection,
        predicateExpression);
}

Теперь я продемонстрирую его использование с EF. Для ясности я должен сначала показать модель предметной области и контекст EF, который я использую. По сути, моя модель - это упрощенный домен блогов и сообщений ... где в блоге есть несколько сообщений, и у каждого сообщения есть дата:

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public DateTime Date { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

Когда этот домен установлен, вот мой код, который в конечном итоге использует исправленный CallAny () и заставляет Linq-to-SQL выполнять работу по оценке Any (). Мой конкретный пример будет сфокусирован на возврате всех блогов, у которых есть хотя бы одно сообщение, которое новее, чем указанная дата окончания.

static void Main()
{
    Database.SetInitializer<BloggingContext>(
        new DropCreateDatabaseAlways<BloggingContext>());

    using (var ctx = new BloggingContext())
    {
        // insert some data
        var blog  = new Blog(){Name = "blog"};
        blog.Posts = new List<Post>() 
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        blog.Posts = new List<Post>()
            { new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } };
        blog.Posts = new List<Post>() 
            { new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } };
        ctx.Blogs.Add(blog);

        blog = new Blog() { Name = "blog 2" };
        blog.Posts = new List<Post>()
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        ctx.Blogs.Add(blog);
        ctx.SaveChanges();


        // first, do a hard-coded Where() with Any(), to demonstrate that
        // Linq-to-SQL can handle it
        var cutoffDateTime = DateTime.Parse("12/31/2001");
        var hardCodedResult = 
            ctx.Blogs.Where((b) => b.Posts.Any((p) => p.Date > cutoffDateTime));
        var hardCodedResultCount = hardCodedResult.ToList().Count;
        Debug.Assert(hardCodedResultCount > 0);


        // now do a logically equivalent Where() with Any(), but programmatically
        // build the expression tree
        var blogsWithRecentPostsExpression = 
            BuildExpressionForBlogsWithRecentPosts(cutoffDateTime);
        var dynamicExpressionResult = 
            ctx.Blogs.Where(blogsWithRecentPostsExpression);
        var dynamicExpressionResultCount = dynamicExpressionResult.ToList().Count;
        Debug.Assert(dynamicExpressionResultCount > 0);
        Debug.Assert(dynamicExpressionResultCount == hardCodedResultCount);
    }
}

Где BuildExpressionForBlogsWithRecentPosts () - это вспомогательная функция, которая использует CallAny () следующим образом:

private Expression<Func<Blog, Boolean>> BuildExpressionForBlogsWithRecentPosts(
    DateTime cutoffDateTime)
{
    var blogParam = Expression.Parameter(typeof(Blog), "b");
    var postParam = Expression.Parameter(typeof(Post), "p");

    // (p) => p.Date > cutoffDateTime
    var left = Expression.Property(postParam, "Date");
    var right = Expression.Constant(cutoffDateTime);
    var dateGreaterThanCutoffExpression = Expression.GreaterThan(left, right);
    var lambdaForTheAnyCallPredicate = 
        Expression.Lambda<Func<Post, Boolean>>(dateGreaterThanCutoffExpression, 
            postParam);

    // (b) => b.Posts.Any((p) => p.Date > cutoffDateTime))
    var collectionProperty = Expression.Property(blogParam, "Posts");
    var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate);
    return Expression.Lambda<Func<Blog, Boolean>>(resultExpression, blogParam);
}

ПРИМЕЧАНИЕ. Я обнаружил еще одну, казалось бы, неважную разницу между жестко запрограммированными и динамически построенными выражениями. У динамически созданной версии есть «дополнительный» вызов convert, которого в жестко запрограммированной версии, похоже, нет (или она не нужна?). Преобразование вводится в реализации CallAny (). Linq-to-SQL, похоже, не против, поэтому я оставил его на месте (хотя в этом не было необходимости). Я не был полностью уверен, может ли это преобразование понадобиться для более надежного использования, чем мой игрушечный образец.

person Aaron Heusser    schedule 08.08.2013
comment
Я мог бы вам сказать, что один - это пункт (1) в моем списке дел, смешанный с уровнями абстракции. Предикат - это значение времени выполнения, а выражение - значение дерева синтаксиса. Прочитав ответ по прошествии 8 лет, я бы исключил преобразование делегата в Expression отдельным методом или на основном сайте вызова. Приблизительные уровни абстракции равны original source -> generic methods -> polymorphic values -> expression-typed values, но последовательность повторяется внутри языка, представленного значениями типа выражений, черепахи полностью вниз. - person Barry Kelly; 09.12.2017