41

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

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

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

4

2 回答 2

85

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

  1. 您正在混合抽象级别。T 参数GetAnyExpression<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,而只是 aExpression的一些实例化类型IEnumerable<>。对于不熟悉编译器 AST 基础的人来说,这是一个常见的错误。弗兰斯·布玛当他第一次开始使用表达式树时,他反复犯了同样的错误——在特殊情况下思考。一般认为。从中期和长期来看,您将为自己省去很多麻烦。

  4. 这就是你的问题的实质(尽管如果你已经解决了第二个问题,也可能是第一个问题,你需要找到合适的 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];
}

因此,给定 any 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();
}
于 2008-11-28T19:32:07.317 回答
19

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

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

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

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

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

  • H结果=-2146233079
  • Message="内部 .NET Framework 数据提供程序错误 1025"
  • 源=系统.数据.实体

在将硬编码的表达式树与使用 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 似乎没问题,所以我把它留在原地(尽管它是不必要的)。我不完全确定在比我的玩具样本更强大的用途中是否需要这种转换。

于 2013-08-08T14:54:05.273 回答