Подтвердить что ты не робот

Как создать дерево выражений, которое вызывает 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);
}

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

4b9b3361

Ответ 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, похоже, в порядке с ним, поэтому я оставил его на месте (хотя это было необязательно). Я не был полностью уверен, что это преобразование может потребоваться в некоторых более надежных целях, чем мой образец игрушки.