Как создать дерево выражений, вызывающее IEnumerable <TSource>.Any (...)? - PullRequest
36 голосов
/ 28 ноября 2008

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

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);
}

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

Ответы [ 2 ]

79 голосов
/ 28 ноября 2008

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

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

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

  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();
}
13 голосов
/ 08 августа 2013

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

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

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

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

Когда я попытался использовать результат 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 (), чтобы она принимала предикатное выражение, а не делегат для логики предиката 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, кажется, в порядке с этим, поэтому я оставил его на месте (хотя это было ненужным). Я не был полностью уверен, что это преобразование может понадобиться при более надежном использовании, чем мой игрушечный образец.

...