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

StackOverflow https://stackoverflow.com/questions/326321

Вопрос

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

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

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

Это было полезно?

Решение

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

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

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

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

  • И вот в чем суть вашей проблемы (хотя вторая и, вероятно, первая проблемы могли бы вас укусить, если бы вы ее преодолели) - вам нужно найти соответствующую общую перегрузку метода 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 > type или какой-то другой тип, но нам нужно найти экземпляр 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];
    }
    

    Итак, учитывая любой Тип , мы теперь можем извлечь из него экземпляр 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() Барри в качестве выражения в предложении Where() Linq-to-SQL, я получил исключение InvalidOperationException со следующими свойствами:

    • HResult=-2146233079
    • Сообщение="Внутренняя ошибка поставщика данных .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);
    }
    

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

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top