Как создать дерево выражений, вызывающее IEnumerable<TSource>.Any(…)?
-
11-07-2019 - |
Вопрос
Я пытаюсь создать дерево выражений, которое представляет следующее:
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 все в порядке, поэтому я оставил его на месте (хотя в этом не было необходимости).Я не был полностью уверен, может ли это преобразование понадобиться в каких-то более надежных приложениях, чем мой игрушечный образец.