如何创建调用 IEnumerable<TSource>.Any(...) 的表达式树?

Posted

技术标签:

【中文标题】如何创建调用 IEnumerable<TSource>.Any(...) 的表达式树?【英文标题】:How do I create an expression tree calling IEnumerable<TSource>.Any(...)? 【发布时间】:2010-09-24 11:44:05 【问题描述】:

我正在尝试创建一个表示以下内容的表达式树:

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

我做错了什么?有人有什么建议吗?

【问题讨论】:

问题核心:***.com/questions/269578/… 【参考方案1】:

您的处理方式有几处问题。

    您正在混合抽象级别。 GetAnyExpression&lt;T&gt; 的 T 参数可能与用于实例化 propertyExp.Type 的类型参数不同。 T 类型参数在抽象堆栈中离编译时间更近一步 - 除非您通过反射调用 GetAnyExpression&lt;T&gt;,否则它将在编译时确定 - 但作为 propertyExp 传递的表达式中嵌入的类型在运行。您将谓词作为Expression 传递也是一种抽象混淆——这是下一点。

    您传递给GetAnyExpression 的谓词应该是一个委托值,而不是任何类型的Expression,因为您正在尝试调用Enumerable.Any&lt;T&gt;。如果您试图调用Any 的表达式树版本,那么您应该传递一个LambdaExpression,您将引用它,这是您可能有理由传递更具体的少数情况之一type 比 Expression 更重要。

    通常,您应该传递Expression 值。通常在使用表达式树时——这适用于所有类型的编译器,而不仅仅是 LINQ 和它的朋友——你应该以一种与你正在使用的节点树的直接组成无关的方式这样做。您假设您正在通过 MemberExpression 呼叫 Any,但实际上您并不需要知道您正在与 MemberExpression 打交道,只是一个Expression 类型的IEnumerable&lt;&gt; 的一些实例化。对于不熟悉编译器 AST 基础的人来说,这是一个常见的错误。 Frans Bouma 在他第一次开始使用表达式树时反复犯了同样的错误——在特殊情况下思考。一般认为。从中长期来看,您会省去很多麻烦。

    这就是你的问题的实质(尽管如果你已经解决了第二个问题,也可能是第一个问题,你需要找到合适的 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&lt;T&gt; 类型或其他类型,但我们需要找到IEnumerable&lt;T&gt; 实例化并获取它的类型参数.我已将其封装成几个函数:

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&lt;T&gt; 实例化 - 并断言是否(完全)没有实例化。

完成这项工作后,解决真正的问题并不太难。我已将您的方法重命名为 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();

【讨论】:

巴里 - 非常感谢您抽出时间向我解释所有这些,非常感谢,我会在周末试一试 :)【参考方案2】:

巴里的回答为原始发帖人提出的问题提供了一个可行的解决方案。感谢这两个人的提问和回答。

我在尝试设计一个非常相似的问题的解决方案时发现了这个线程:以编程方式创建一个包含对 Any() 方法的调用的表达式树。然而,作为一个额外的限制,我的解决方案的最终目标是通过 Linq-to-SQL 传递这样一个动态创建的表达式,以便 Any() 评估的工作实际上在数据库本身。

不幸的是,到目前为止讨论的解决方案不是 Linq-to-SQL 可以处理的。

假设这可能是想要构建动态表达式树的一个非常受欢迎的原因,我决定用我的发现来扩充线程。

当我尝试使用 Barry 的 CallAny() 的结果作为 Linq-to-SQL Where() 子句中的表达式时,我收到了具有以下属性的 InvalidOperationException:

HResult=-2146233079 Message="内部 .NET Framework 数据提供程序错误 1025" Source=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 似乎没问题,所以我把它留在原处(尽管它是不必要的)。我不完全确定在比我的玩具样本更强大的用途中是否需要这种转换。

【讨论】:

我本可以告诉你一个 - 它是我要做的事情列表中的第 (1) 项,混合了抽象级别。谓词是运行时值,但表达式是语法树值。在 8 年后阅读答案,我会在单独的方法中或在主调用站点中将委托转换为表达式。抽象的近似级别是original source -&gt; generic methods -&gt; polymorphic values -&gt; expression-typed values,但序列在表达式类型值表示的语言中重复自身,一直向下。

以上是关于如何创建调用 IEnumerable<TSource>.Any(...) 的表达式树?的主要内容,如果未能解决你的问题,请参考以下文章

如何判断 IEnumerable<T> 是不是需要延迟执行?

如何在 Entity Framework Core 2.0 中从 DbDataReader 转换为 Task<IEnumerable<TEntity>>?

C#- 在任务异步调用上返回 IEnumerable<object>

作为异步函数调用返回IEnumerable

从 IEnumerable<KeyValuePair<>> 重新创建字典

如何将多个 IEnumerable<IEnumerable<T>> 列表添加到 IEnumerable<List<int>>