Как создать дерево выражений, которое вызывает IEnumerable .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  the //MethodInfo object is always null - I can't get a reference to it private static MethodCallExpression GetAnyExpression(MemberExpression propertyExp, Expression predicateExp) { MethodInfo method = typeof(Enumerable).GetMethod("Any", new[]{ typeof(Func<IEnumerable, Boolean>)}); return Expression.Call(propertyExp, method, predicateExp); } 

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

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

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

  2. Предикат, который вы передаете GetAnyExpression должен быть значением делегата, а не Expression любого типа, поскольку вы пытаетесь вызвать Enumerable.Any . Если вы пытались вызвать версию дерева выражений 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 или какого-либо другого типа, но нам нужно найти экземпляр IEnumerable и получить его аргумент типа. Я инкапсулировал это в пару функций:

 static bool IsIEnumerable(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>); } static Type GetIEnumerableImpl(Type type) { // Get IEnumerable implementation. Either type is IEnumerable for some T, // or it implements IEnumerable 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 и утверждать, если его нет (точно).

С учетом этой работы решение реальной проблемы не слишком сложно. Я переименовал ваш метод в 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(IEnumerable, Func) 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 strings = new List { "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 a = (Func) Expression.Lambda(anyCall).Compile(); Console.WriteLine("Found? {0}", a()); Console.ReadLine(); } 

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

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

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

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

Когда я попытался использовать результат 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(IEnumerable, Func) 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 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 Blogs { get; set; } public DbSet Posts { get; set; } } 

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

 static void Main() { Database.SetInitializer( new DropCreateDatabaseAlways()); using (var ctx = new BloggingContext()) { // insert some data var blog = new Blog(){Name = "blog"}; blog.Posts = new List() { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } }; blog.Posts = new List() { new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } }; blog.Posts = new List() { new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } }; ctx.Blogs.Add(blog); blog = new Blog() { Name = "blog 2" }; blog.Posts = new List() { 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> 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>(dateGreaterThanCutoffExpression, postParam); // (b) => b.Posts.Any((p) => p.Date > cutoffDateTime)) var collectionProperty = Expression.Property(blogParam, "Posts"); var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate); return Expression.Lambda>(resultExpression, blogParam); } 

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

  • Какой метод работает лучше: .Any () vs .Count ()> 0?
  • Выполнение поиска () по сравнению с FirstOrDefault ()
  • LINQ to Entities не распознает метод 'System.String Format (System.String, System.Object, System.Object)'
  • Преобразование DataTable в общий список в C #
  • Как сделать полное внешнее соединение в Linq?
  • Как проверить нули в глубоком lambda-выражении?
  • Linq to Entities соединяются с groupjoin
  • Невозможно создать постоянное значение - только примитивные типы
  • Как объединить два списка с помощью LINQ?
  • Преобразовать выражение Linq "obj => obj.Prop" в "parent => parent.obj.Prop"
  • Невозможно преобразовать lambda-выражение для ввода «string», потому что это не тип делегата
  • Interesting Posts

    Где читать консольные сообщения из background.js в расширении Chrome?

    Откройте прямой файл на жестком диске из Firefox (файл: ///)

    Разверните div с середины, а не сверху и слева, используя CSS

    Встроенный блок IE8 не работает

    Что-то не так с моим дисковым разделом?

    Wireshark: доступ к интерфейсам шины USB без sudo

    Сохранить изображение в папку документов приложений из UIView на IOS

    Возможность установки буквы диска

    Загрузочный стол с полосками: как изменить цвет фона полосы?

    Как вы обновляете файл Excel (формулы обновления и обновления данных) БЕЗ открытия файла?

    Почему удаляет огромное количество файлов (в среднем малый размер) дольше, чем один файл (огромный размер)

    Хороший размер блока для клонирования диска с помощью diskdump (dd)

    Как предотвратить двойной щелчок при помощи jQuery?

    Есть ли веская причина использовать «printf» вместо «print» в java?

    Dlink DIR-615 неожиданно перестает работать – оранжевый интернет-свет

    Давайте будем гением компьютера.