Как создать дерево выражений, которое вызывает IEnumerable <TSource>.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 <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
также является смешением абстракции, которое является следующей точкой.
-
Предикат, который вы передаете GetAnyExpression
, должен быть значением делегата, а не Expression
любого типа, поскольку вы пытаетесь вызвать Enumerable.Any<T>
. Если вы пытались вызвать версию дерева выражений Any
, то вам следует передать LambdaExpression
вместо этого, который вы цитируете, и является одним из редких случаев, когда вы можете быть оправданы при передаче более конкретной типа Expression, что приводит меня к следующему пункту.
-
В общем, вы должны передавать значения Expression
. При работе с деревьями выражений вообще - и это применимо ко всем видам компиляторов, а не только к LINQ и его друзьям - вы должны сделать это так, чтобы агностик относительно непосредственного состава дерева node, с которым вы работаете. Вы предполагаете, что вы вызываете Any
на MemberExpression
, но на самом деле вам не нужно знать, что вы имеете дело с MemberExpression
, просто a Expression
типа некоторого экземпляра IEnumerable<>
. Это распространенная ошибка для людей, не знакомых с основами АСТ. Франс Бума неоднократно совершал ту же ошибку, когда он впервые начал работать с деревьями выражений - в особых случаях. Подумайте вообще. Вы сэкономите много хлопот в среднесрочной и долгосрочной перспективе.
-
И здесь идет мясо вашей проблемы (хотя вторая и, вероятно, первая проблема будет бит вам, если вы ее пропустили) - вам нужно найти соответствующую общую перегрузку метода 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));
}
Здесь a 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();
}
Ответ 2
Ответ Барри обеспечивает рабочее решение вопроса, заданного оригинальным плакатом. Спасибо всем тем, кто просит и отвечает.
Я нашел этот поток, поскольку пытался придумать решение довольно схожей проблемы: программно создавая дерево выражений, которое включает вызов метода Any(). В качестве дополнительного ограничения, однако, конечная цель моего решения заключалась в том, чтобы передать такое динамически созданное выражение через Linq-to-SQL, чтобы работа оценки Any() фактически выполнялась в Сам БД.
К сожалению, решение, как обсуждалось до сих пор, не является тем, с чем может справиться Linq-to-SQL.
Работая в предположении, что это может быть довольно популярной причиной для желания создать динамическое дерево выражений, я решил увеличить поток с моими выводами.
Когда я попытался использовать результат Barry CallAny() как выражение в предложении Linq-to-SQL Where(), я получил исключение InvalidOperationException со следующими свойствами:
- HResult = -2146233079
- Сообщение = "Внутренняя ошибка поставщика данных .NET Framework 1025"
- Source = System.Data.Entity
После сравнения дерева с жестко закодированным выражением с динамически созданным с помощью CallAny() я обнаружил, что основная проблема связана с компилятором() выражения предиката и попыткой вызвать получателя в 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, который я использую. В основном моя модель является упрощенным блогами Blogs and 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, похоже, в порядке с ним, поэтому я оставил его на месте (хотя это было необязательно). Я не был полностью уверен, что это преобразование может потребоваться в некоторых более надежных целях, чем мой образец игрушки.