我正在尝试创建一个表示以下内容的表达式树:
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 usethe //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); }
我究竟做错了什么?有人有什么建议吗?
如何处理它有几个问题.
你正在混合抽象级别.T参数GetAnyExpression
可以与用于实例化的类型参数不同propertyExp.Type
.T类型参数在抽象堆栈中更接近编译时间 - 除非您GetAnyExpression
通过反射调用,它将在编译时确定 - 但传递的表达式中嵌入的类型propertyExp
是在运行时确定的.你传递谓词Expression
也是一种抽象混淆 - 这是下一点.
您传递的谓词GetAnyExpression
应该是委托值,而不是Expression
任何类型,因为您正在尝试调用Enumerable.Any
.如果你试图调用表达式树版本Any
,那么你应该传递一个LambdaExpression
相反的,你会引用它,并且是一种罕见的情况,你可能有理由传递一个比Expression更具体的类型,这导致我到下一点.
通常,您应该传递Expression
值.通常使用表达式树时 - 这适用于所有类型的编译器,而不仅仅是LINQ及其朋友 - 您应该以与您正在使用的节点树的直接组成无关的方式这样做.你假设你正在调用Any
a 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 IEnumerablefor 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 Liststrings = 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(); }
巴里的回答为原始海报提出的问题提供了有效的解决方案.感谢这两个人的询问和回答.
我找到了这个线程,因为我试图为一个非常类似的问题设计一个解决方案:以编程方式创建一个包含对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 ListPosts { 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似乎没问题所以我把它留在原地(尽管没必要).我不完全确定在一些比我的玩具样本更强大的用法中是否需要这种转换.