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

43

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

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

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

问题的核心:https://dev59.com/BnVC5IYBdhLWcg3whBej - nawfal
2个回答

88
你在处理这个问题的方法中存在几个问题。
  1. 你混淆了抽象层级。传递给 GetAnyExpression<T> 的 T 参数可能与用于实例化 propertyExp.Type 的类型参数不同。T 类型参数在抽象堆栈中更接近编译时 - 除非通过反射调用 GetAnyExpression<T>,否则它将在编译时确定 - 但嵌入在传递给 propertyExp 的表达式中的类型在运行时确定。你将谓词作为 Expression 传递也是一个抽象混乱 - 这是下一个问题。

  2. 你传递给 GetAnyExpression 的谓词应该是一个委托值,而不是任何类型的 Expression,因为你正在尝试调用 Enumerable.Any<T>。如果你想调用一个表达式树版本的 Any,那么你应该传递一个 LambdaExpression,并对其进行引用,这是你可能有正当理由传递比 Expression 更具体的类型的罕见情况之一,这将导致我的下一个观点。

  3. 通常情况下,你应该传递 Expression 值。当处理表达式树时 - 这适用于所有类型的编译器,不仅仅是 LINQ 及其友好工具 - 你应该以一种对你正在处理的节点树的组成方式无视的方式处理它们。你“假定”你在调用 Any 上有一个 MemberExpression,但你实际上不需要知道你正在处理一个 MemberExpression,只需要一个类型为一些 IEnumerator<> 实例化的 Expression。对于不熟悉编译器 AST 基础知识的人来说,这是一个常见错误。Frans Bouma 在他开始使用表达式树时反复犯了同样的错误 - 思考特殊情况。要通用思考。中长期内这将为你省去很多麻烦。

  4. 这就是你问题的核心(尽管第二个和可能第一个问题会困扰你) - 你需要找到合适的 Any 方法的泛型重载,然后使用正确的类型进行实例化。Reflection 在这里无法提供简单的解决方案;你需要迭代查找合适的版本。

因此,将其分解:你需要找到一个泛型方法(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表达式中获取这些并不完全容易,因为表达式可能是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));
}

这是一个使用上述所有代码并验证其在简单情况下是否有效的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();
}

5
Barry-非常感谢你抽出时间向我解释所有这些内容,万分感谢。这个周末我会尝试一下 :) - flesh

20

Barry的回答提供了一个解决原帖中提出的问题的可行方法。感谢这两个人的提问和回答。

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

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

基于这可能是想要构建动态表达式树的一个相当普遍的原因的假设,我决定用我的发现来增强这个线程。

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

  • HResult=-2146233079
  • Message="Internal .NET Framework Data Provider error 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似乎可以处理它,所以我保留了它(尽管它是不必要的)。我并不完全确定这种转换是否在比我的玩具示例更健壮的用法中可能需要。


我本可以告诉你这个问题的答案 - 它是我“要做的事情清单”中的第一个,混合抽象级别。谓词是运行时值,但表达式是语法树值。在8年后阅读答案后,我会将委托转换为表达式的过程分离到一个单独的方法中,或者在主调用站点上进行。抽象级别的近似水平为“原始源->通用方法->多态值->表达式类型的值”,但该序列在由表达式类型的值表示的语言内部重复出现,一直延续下去。 - Barry Kelly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接