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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/326321/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 23:17:50  来源:igfitidea点击:

How do I create an expression tree calling IEnumerable<TSource>.Any(...)?

c#.netlinqexpression-trees

提问by flesh

I am trying to create an expression tree that represents the following:

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

myObject.childObjectCollection.Any(i => i.Name == "name");

Shortened for clarity, I have the following:

为了清楚起见,我有以下几点:

//'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);
}

What am I doing wrong? Anyone have any suggestions?

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

采纳答案by Barry Kelly

There are several things wrong with how you're going about it.

你的处理方式有几个问题。

  1. You're mixing abstraction levels. The T parameter to GetAnyExpression<T>could be different to the type parameter used to instantiate propertyExp.Type. The T type parameter is one step closer in the abstraction stack to compile time - unless you're calling GetAnyExpression<T>via reflection, it will be determined at compile time - but the type embedded in the expression passed as propertyExpis determined at runtime. Your passing of the predicate as an Expressionis also an abstraction mixup - which is the next point.

  2. The predicate you are passing to GetAnyExpressionshould be a delegate value, not an Expressionof any kind, since you're trying to call Enumerable.Any<T>. If you were trying to call an expression-tree version of Any, then you ought to pass a LambdaExpressioninstead, which you would be quoting, and is one of the rare cases where you might be justified in passing a more specific type than Expression, which leads me to my next point.

  3. In general, you should pass around Expressionvalues. When working with expression trees in general - and this applies across all kinds of compilers, not just LINQ and its friends - you should do so in a way that's agnostic as to the immediate composition of the node tree you're working with. You are presumingthat you're calling Anyon a MemberExpression, but you don't actually need to knowthat you're dealing with a MemberExpression, just an Expressionof type some instantiation of IEnumerable<>. This is a common mistake for people not familiar with the basics of compiler ASTs. Frans Boumarepeatedly made the same mistake when he first started working with expression trees - thinking in special cases. Think generally. You'll save yourself a lot of hassle in the medium and longer term.

  4. And here comes the meat of your problem (though the second and probably first issues would have bit you if you had gotten past it) - you need to find the appropriate generic overload of the Any method, and then instantiate it with the correct type. Reflection doesn't provide you with an easy out here; you need to iterate through and find an appropriate version.

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

  2. 您传递给的谓词GetAnyExpression应该是一个委托值,而不是Expression任何类型的,因为您试图调用Enumerable.Any<T>. 如果您试图调用 的表达式树版本Any,那么您应该传递 aLambdaExpression代替,您将引用它,并且是您可能有理由传递比 Expression 更具体的类型的罕见情况之一,这会导致我到我的下一点。

  3. 通常,您应该传递Expression值。通常在使用表达式树时 - 这适用于所有类型的编译器,而不仅仅是 LINQ 及其朋友 - 您应该以一种与您正在使用的节点树的直接组合无关的方式进行操作。您假设您正在调用Anya MemberExpression,但您实际上并不需要知道您正在处理 a MemberExpression,只是Expression类型 的某个实例化的an IEnumerable<>。对于不熟悉编译器 AST 基础知识的人来说,这是一个常见的错误。弗兰斯·鲍马当他第一次开始使用表达式树时,他反复犯同样的错误——在特殊情况下思考。一般认为。从中期和长期来看,您将为自己省去很多麻烦。

  4. 问题的实质就来了(尽管如果您已经解决了第二个问题,也可能是第一个问题) - 您需要找到 Any 方法的适当泛型重载,然后使用正确的类型实例化它。反射并没有为您提供一个简单的方法。您需要遍历并找到合适的版本。

So, breaking it down: you need to find a generic method (Any). Here's a utility function that does that:

因此,分解一下:您需要找到一个通用方法 ( 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);
}

However, it requires the type arguments and the correct argument types. Getting that from your propertyExpExpressionisn't entirely trivial, because the Expressionmay be of a List<T>type, or some other type, but we need to find the IEnumerable<T>instantiation and get its type argument. I've encapsulated that into a couple of functions:

但是,它需要类型参数和正确的参数类型。从 your 中获取它propertyExpExpression并不完全是微不足道的,因为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];
}

So, given any Type, we can now pull the IEnumerable<T>instantiation out of it - and assert if there isn't (exactly) one.

因此,给定 any Type,我们现在可以从中提取IEnumerable<T>实例化 - 并断言是否(完全)没有实例化。

With that work out of the way, solving the real problem isn't too difficult. I've renamed your method to CallAny, and changed the parameter types as suggested:

有了这些工作,解决真正的问题就不太困难了。我已将您的方法重命名为 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));
}

Here's a Main()routine which uses all the above code and verifies that it works for a trivial case:

这是一个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();
}

回答by Aaron Heusser

Barry's answer provides a working solution to the question posed by the original poster. Thanks to both of those individuals for asking and answering.

Barry 的回答为原始海报提出的问题提供了可行的解决方案。感谢这两个人的提问和回答。

I found this thread as I was trying to devise a solution to a quite similar problem: programmatically creating an expression tree that includes a call to the Any() method. As an additional constraint, however, the ultimate goalof my solution was to pass such a dynamically-created expression through Linq-to-SQL so that the work of the Any() evaluation is actually performed in the DB itself.

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

Unfortunately, the solution as discussed so far is not something that Linq-to-SQL can handle.

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

Operating under the assumption that this might be a pretty popular reason for wanting to build a dynamic expression tree, I decided to augment the thread with my findings.

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

When I attempted to use the result of Barry's CallAny() as an expression in a Linq-to-SQL Where() clause, I received an InvalidOperationException with the following properties:

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

  • HResult=-2146233079
  • Message="Internal .NET Framework Data Provider error 1025"
  • Source=System.Data.Entity
  • HResult=-2146233079
  • 消息="内部 .NET Framework 数据提供程序错误 1025"
  • 源=系统.数据.实体

After comparing a hard-coded expression tree to the dynamically-created one using CallAny(), I found that the core problem was due to the Compile() of the predicate expression and the attempt to invoke the resulting delegate in the CallAny(). Without digging deep into Linq-to-SQL implementation details, it seemed reasonable to me that Linq-to-SQL wouldn't know what to do with such a structure.

在将硬编码的表达式树与使用 CallAny() 动态创建的表达式树进行比较后,我发现核心问题是由于谓词表达式的 Compile() 以及在 CallAny() 中调用结果委托的尝试。如果不深入研究 Linq-to-SQL 的实现细节,我认为 Linq-to-SQL 不知道如何处理这样的结构似乎是合理的。

Therefore, after some experimentation, I was able to achieve my desired goal by slightly revising the suggested CallAny() implementation to take a predicateExpression rather than a delegate for the Any() predicate logic.

因此,经过一些实验,我能够通过稍微修改建议的 CallAny() 实现以采用 predicateExpression 而不是 Any() 谓词逻辑的委托来实现我想要的目标。

My revised method is:

我修改的方法是:

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

Now I will demonstrate its usage with EF. For clarity I should first show the toy domain model & EF context I am using. Basically my model is a simplistic Blogs & Posts domain ... where a blog has multiple posts and each post has a date:

现在我将演示它与 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; }
}

With that domain established, here is my code to ultimately exercise the revised CallAny() and make Linq-to-SQL do the work of evaluating the Any(). My particular example will focus on returning all Blogs which have at least one Post that is newer than a specified cutoff date.

建立该域后,这里是我的代码,用于最终执行修改后的 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);
    }
}

Where BuildExpressionForBlogsWithRecentPosts() is a helper function that uses CallAny() as follows:

其中 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);
}

NOTE: I found one other seemingly unimportant delta between the hard-coded and dynamically-built expressions. The dynamically-built one has an "extra" convert call in it that the hard-coded version doesn't seem to have (or need?). The conversion is introduced in the CallAny() implementation. Linq-to-SQL seems to be ok with it so I left it in place (although it was unnecessary). I was not entirely certain if this conversion might be needed in some more robust usages than my toy sample.

注意:我在硬编码和动态构建的表达式之间发现了另一个看似不重要的增量。动态构建的有一个“额外”的转换调用,硬编码版本似乎没有(或不需要?)。CallAny() 实现中引入了转换。Linq-to-SQL 似乎没问题,所以我把它留在原地(尽管这是不必要的)。我并不完全确定在一些比我的玩具样本更强大的用途中是否可能需要这种转换。