当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《如何创建一个调用IEnumerable<TSource>.Any()的表达式树?》经验,为你挑选了2个好方法。

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

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  the 
//MethodInfo object is always null - I can't get a reference to it

private static MethodCallExpression GetAnyExpression(MemberExpression propertyExp, Expression predicateExp)
{
    MethodInfo method = typeof(Enumerable).GetMethod("Any", new[]{ typeof(Func, Boolean>)});
    return Expression.Call(propertyExp, method, predicateExp);
}

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



1> Barry Kelly..:

如何处理它有几个问题.

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

    您传递的谓词GetAnyExpression应该是委托值,而不是Expression任何类型,因为您正在尝试调用Enumerable.Any.如果你试图调用表达式树版本Any,那么你应该传递一个LambdaExpression相反的,你会引用它,并且是一种罕见的情况,你可能有理由传递一个比Expression更具体的类型,这导致我到下一点.

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

    这就是你问题的关键所在(虽然第二个问题可能是第一个问题,但如果你已经超过了它就会有点问题) - 你需要找到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类型,或者其他类型,但是我们需要找到IEnumerable实例化并获得它的类型参数.我把它封装成了几个函数:

static bool IsIEnumerable(Type type)
{
    return type.IsGenericType
        && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}

static Type GetIEnumerableImpl(Type type)
{
    // Get IEnumerable implementation. Either type is IEnumerable for some T, 
    // or it implements IEnumerable 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实例化从中拉出来 - 如果没有(确切地),则断言.

通过这项工作,解决真正的问题并不困难.我已将您的方法重命名为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(IEnumerable, Func)
    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 strings = new List { "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 a = (Func) Expression.Lambda(anyCall).Compile();
    Console.WriteLine("Found? {0}", a());
    Console.ReadLine();
}


巴里 - 我真的很感谢你花时间向我解释所有这些,非常感谢,我会在周末给它一个破解:)

2> Aaron Heusse..:

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

我找到了这个线程,因为我试图为一个非常类似的问题设计一个解决方案:以编程方式创建一个包含对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(IEnumerable, Func)
    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 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 Blogs { get; set; }
    public DbSet Posts { get; set; }
}

建立该域后,这里是我的代码,最终执行修订后的CallAny()并使Linq-to-SQL完成评估Any()的工作.我的特定示例将重点关注返回所有至少有一个比指定截止日期更新的帖子的博客.

static void Main()
{
    Database.SetInitializer(
        new DropCreateDatabaseAlways());

    using (var ctx = new BloggingContext())
    {
        // insert some data
        var blog  = new Blog(){Name = "blog"};
        blog.Posts = new List() 
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        blog.Posts = new List()
            { new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } };
        blog.Posts = new List() 
            { new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } };
        ctx.Blogs.Add(blog);

        blog = new Blog() { Name = "blog 2" };
        blog.Posts = new List()
            { 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> 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>(dateGreaterThanCutoffExpression, 
            postParam);

    // (b) => b.Posts.Any((p) => p.Date > cutoffDateTime))
    var collectionProperty = Expression.Property(blogParam, "Posts");
    var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate);
    return Expression.Lambda>(resultExpression, blogParam);
}

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

推荐阅读
echo7111436
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有