簡體   English   中英

如何創建一個調用IEnumerable <TSource> .Any(...)的表達式樹?

[英]How do I create an expression tree calling IEnumerable<TSource>.Any(…)?

我正在嘗試創建一個表示以下內容的表達式樹:

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

我究竟做錯了什么? 有人有什么建議嗎?

如何處理它有幾個問題。

  1. 你正在混合抽象級別。 GetAnyExpression<T>的T參數可能與用於實例化propertyExp.Type的類型參數不同。 T類型參數在抽象堆棧中更接近編譯時間 - 除非您通過反射調用GetAnyExpression<T> ,它將在編譯時確定 - 但是作為propertyExp傳遞的表達式中嵌入的類型是在運行時確定的。 將謂詞作為Expression傳遞也是一種抽象混淆 - 這是下一點。

  2. 傳遞給GetAnyExpression的謂詞應該是委托值,而不是任何類型的Expression ,因為您嘗試調用Enumerable.Any<T> 如果你試圖調用Any的表達式樹版本,那么你應該傳遞一個LambdaExpression ,你將引用它,並且是一種極少數情況下你可能有理由傳遞一個比Expression更具體的類型,這讓我想到了下一點。

  3. 通常,您應該傳遞Expression值。 通常使用表達式樹時 - 這適用於所有類型的編譯器,而不僅僅是LINQ及其朋友 - 您應該以與您正在使用的節點樹的直接組成無關的方式這樣做。 假設你在一個MemberExpression上調用Any ,但你實際上並不需要知道你正在處理一個MemberExpression ,只是一個類型的Expression IEnumerable<>實例化。 對於不熟悉編譯器AST基礎知識的人來說,這是一個常見的錯誤。 Frans Bouma在他第一次開始使用表達樹時反復犯了同樣的錯誤 - 在特殊情況下思考。 一般來說。 從中長期來看,你會省去很多麻煩。

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

因此,給定任何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();
}

巴里的回答為原始海報提出的問題提供了有效的解決方案。 感謝這兩個人的詢問和回答。

我找到了這個線程,因為我試圖為一個非常類似的問題設計一個解決方案:以編程方式創建一個包含對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演示它的用法。 為清楚起見,我應首先顯示我正在使用的玩具域模型和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似乎沒問題所以我把它留在原地(盡管沒必要)。 我不完全確定在一些比我的玩具樣本更強大的用法中是否需要這種轉換。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM