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

时间:2008-11-28 17:55:21

标签: c# .net linq expression-trees

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

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

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

2 个答案:

答案 0 :(得分:79)

你如何处理它有几个问题。

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

  2. 您传递给Expression的谓词应该是委托值,而不是任何类型的GetAnyExpression,因为您尝试拨打Expression。如果您尝试调用Enumerable.Any<T>的表达式树版本,那么您应该传递一个Any代替,这将是您所引用的,并且是您可能在其中证明其合理性的极少数情况之一传递比Expression更具体的类型,这引导我进入下一点。

  3. 通常,您应该传递LambdaExpression个值。通常使用表达式树时 - 这适用于所有类型的编译器,而不仅仅是LINQ及其朋友 - 您应该以与您正在使用的节点树的直接组成无关的方式这样做。您假设您在Expression上呼叫Any,但实际上需要知道您正在处理一个MemberExpression,只是MemberExpression类型Expression的一些实例。对于不熟悉编译器AST基础知识的人来说,这是一个常见的错误。 Frans Bouma在他第一次开始使用表达树时反复犯了同样的错误 - 在特殊情况下思考。一般来说。你会在中长期内为自己省去很多麻烦。

  4. 这就是你问题的关键所在(虽然第二个也可能是第一个问题,如果你已经超越了它会让你感到厌烦) - 你需要找到Any方法的相应泛型重载,然后实例化它具有正确的类型。反思并没有为你提供一个简单的方法;你需要迭代并找到合适的版本。

  5. 所以,分解它:你需要找到一个通用的方法(IEnumerable<>)。这是一个实用功能:

    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类型,或其他类型,但我们需要找到{ {1}}实例化并获取其类型参数。我把它封装成了几个函数:

    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]; } 实例化从中拉出 - 并断言是否存在(确切地说)。

    完成这项工作后,解决实际问题并不困难。我已将您的方法重命名为CallAny,并按照建议更改了参数类型:

    Type

    这是一个IEnumerable<T>例程,它使用上述所有代码并验证它是否适用于一个简单的案例:

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

答案 1 :(得分:13)

巴里的回答为原始海报提出的问题提供了有效的解决方案。感谢这两个人提问和回答。

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

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

假设这可能是想要构建动态表达式树的一个非常流行的原因,我决定用我的发现来增加线程。

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

  • 的HResult = -2146233079
  • Message =“内部.NET Framework数据提供程序错误1025”
  • 源= System.Data.Entity的

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

因此,经过一些实验,我能够通过稍微修改建议的CallAny()实现来实现我想要的目标,以获取谓词表达而不是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中的用法。为清楚起见,我应首先展示玩具领域模型&amp;我正在使用的EF上下文。基本上我的模型是一个简单的博客&amp;帖子域名...博客有多个帖子,每个帖子都有一个日期:

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似乎没问题所以我把它留在原地(尽管没必要)。我不完全确定在一些比我的玩具样本更强大的用法中是否需要这种转换。