metanit - деревья выражений c#




Как создать дерево выражений, которое вызывает IEnumerable<TSource>.Any(...)? (2)

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

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

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


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

  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 типа some instantiation of IEnumerable<> . Это распространенная ошибка для людей, не знакомых с основами АСТ. Франс Бума неоднократно совершал ту же ошибку, когда впервые начал работать с деревьями выражений - в особых случаях. Подумайте вообще. Вы сэкономите много хлопот в среднесрочной и долгосрочной перспективе.

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

Однако для этого требуются аргументы типа и правильные типы аргументов. Получение этого из Expression propertyExp не является тривиальным, потому что 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();
}

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

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

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

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

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

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

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

Поэтому после некоторых экспериментов я смог достичь желаемой цели, слегка переработав предложенную реализацию CallAny (), чтобы использовать предикатExpression, а не делегат для логики предикатов 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, который я использую. В основном моя модель является упрощенным блоком Blogs & Posts ... где в блоге есть несколько сообщений, и у каждого сообщения есть дата:

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

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





expression-trees