构建 GroupBy 表达式树 – 未定义 IEnumerable 参数错误

huangapple go评论48阅读模式
英文:

Building GroupBy Expression Tree - IEnumerable parameter not defined error

问题

I want to build an expression for IQueryable GroupBy. While at the moment I'm just simplifying the problem to try and get it working, the eventual final implementation will involve the creation of quite complex expression trees so I want to build a complete expression that can then be integrated into other expressions.

我想构建一个用于IQueryable GroupBy的表达式。虽然目前我只是在简化问题以尝试让它工作,但最终的实现将涉及创建相当复杂的表达式树,因此我希望构建一个完整的表达式,然后可以将其集成到其他表达式中。

I specifically want to build an expression of this overload:

我特别想构建这个重载的表达式:

public static System.Linq.IQueryable<TResult> GroupBy<TSource,TKey,TResult> (
    this System.Linq.IQueryable<TSource> source, 
    System.Linq.Expressions.Expression<Func<TSource,TKey>> keySelector, 
    System.Linq.Expressions.Expression<Func<TKey,System.Collections.Generic.IEnumerable<TSource>,TResult>> resultSelector);

... my problem is in the implementation of the resultSelector and and the IEnumerable<TSource>.

...我的问题在于resultSelector和IEnumerable<TSource>的实现。

I have a table of Customers (just dummy data for the purposes of working out this problem). This is stored in an SQL DB and I specifically want to use IQueryable to access the data.

我有一个客户表(只是为了解决这个问题而使用的虚拟数据)。这些数据存储在SQL数据库中,我特别希望使用IQueryable来访问数据。

public class Customer
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public int Age { get; set; }
}

I also have a GroupResult class used to hold the results of the GroupBy (I have different constructors which I've been using in my testing to work out where my problem is occurring)

我还有一个GroupResult类,用于保存GroupBy的结果(我有不同的构造函数,我一直在测试中使用它们来找出问题发生在哪里)

internal class GroupResult
{
    public string? Name { get; set; }
    public int NumRecords { get; set; }
    public decimal AverageAge { get; set; }
    public int TotalAge { get; set; }

    public GroupResult() { }
    
    public GroupResult(string name)
    {
        Name = name;
    }

    public GroupResult(IEnumerable<Customer> customers)
    {
        Name = Guid.NewGuid().ToString();
        NumRecords = customers.Count();
    }

    public GroupResult(string name, IEnumerable<Customer> customers)
    {
        Name = name;
        NumRecords = customers.Count();
    }

}

The main static class that displays prompts to select column to group on, creates the relevant expression tree and executes it

主要的静态类显示提示,选择要分组的列,创建相关的表达式树并执行它

internal static class SimpleGroupByCustomer
{
    internal static DataContext db;

    internal static void Execute()
    {
        using (db = new DataContext())
        {
            //get input
            Console.WriteLine();
            Console.WriteLine("Simple Customer GroupBy");
            Console.WriteLine("=======================");
            Console.WriteLine("Simple GroupBy on the Customer Table");
            Console.WriteLine();

            Console.WriteLine("Select the property that you want to group by.");
            Console.WriteLine();

            var dbSet = db.Set<Customer>();
            var query = dbSet.AsQueryable();

            //for this example we're just prompting for a column in the customer table
            //GetColumnName is a helper function that lists the available columns and allows
            //one to be selected
            string colName = Wrapper.GetColumnName("Customer");

            MethodInfo? method = typeof(SimpleGroupByCustomer).GetMethod("GetGroupBy",
                BindingFlags.Static | BindingFlags.NonPublic);

            if (method != null)
            {
                method = method.MakeGenericMethod(new Type[] { typeof(String), query.ElementType });
                method.Invoke(null, new object[] { query, colName });
            }
        }
    }

    internal static void GetGroupBy<T, TTable>(IQueryable query, string colName)
    {
        Type TTmp = typeof(TTable);
        var param = Expression.Parameter(TTmp, "c");
        var prop = Expression.PropertyOrField(param, colName);

        LambdaExpression keySelector = Expression.Lambda<Func<TTable, T>>(prop, param);

        var param1 = Expression.Parameter(typeof(T), "Key");
        var param2 = Expression.Parameter(typeof(IEnumerable<TTable>), "Customers");

        var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T), typeof(IEnumerable<TTable>) });
        //var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T) });
        //var ci = typeof(GroupResult).GetConstructor(new[] { typeof(IEnumerable<TTable>) });

        if (ci == null)
            return;

        var pExp = new ParameterExpression[] { param1, param2 };

        var methodExpression = Expression.Lambda<Func<T, IEnumerable<TTable>, GroupResult>>(
            Expression.New(ci, new Expression[] { param1, param2 }), //<--- ERROR HERE
            pExp
            );

        Type[] typeArgs = new Type[] { typeof(TTable), typeof(T), typeof(GroupResult) };
        Expression[] methodParams = new Expression[] { query.Expression, keySelector, methodExpression };

        var resultExpression = Expression.Call(typeof(Queryable), "GroupBy", typeArgs, methodParams);

        IQueryable dbQuery = query.Provider.CreateQuery(resultExpression);

        if (dbQuery is IQueryable<GroupResult> results)
        {
            foreach (var result in results)
            {
                Console.WriteLine("{0,-15}\t{1}", result.Name, result.NumRecords.ToString());
            }
        }
    }
}

When I run this and try and iterate through the results I get the following exception:

当我运行这个程序并尝试遍历结果时,我会得到以下异常:

> System.InvalidOperationException: 'variable 'Customers' of type 'System.Collections.Generic.IEnumerable`1[ExpressionTrees3.Data.Customer]' referenced from scope '', but it is not defined'

英文:

I want to build an expression for IQueryable GroupBy. While at the moment I'm just simplifying the problem to try and get it working, the eventual final implementation will involve the creation of quite complex expression trees so I want to build a complete expression that can then be integrated into other expressions.

I specifically want to build an expression of this overload:

public static System.Linq.IQueryable<TResult> GroupBy<TSource,TKey,TResult> (
    this System.Linq.IQueryable<TSource> source, 
    System.Linq.Expressions.Expression<Func<TSource,TKey>> keySelector, 
    System.Linq.Expressions.Expression<Func<TKey,System.Collections.Generic.IEnumerable<TSource>,TResult>> resultSelector);

... my problem is in the implementation of the resultSelector and and the IEnumerable<TSource>.

I have a table of Customers (just dummy data for the purposes of working out this problem). This is stored in an SQL DB and I specifically want to use IQueryable to access the data.

public class Customer
{
    public int Id { get; set; }
    public string? FirstName { get; set; }
    public string? LastName { get; set; }
    public int Age { get; set; }
}

I also have a GroupResult class used to hold the results of the GroupBy (I have different constructors which I've been using in my testing to work out where my problem is occurring)

internal class GroupResult
{
    public string? Name { get; set; }
    public int NumRecords { get; set; }
    public decimal AverageAge { get; set; }
    public int TotalAge { get; set; }

    public GroupResult() { }
    
    public GroupResult(string name)
    {
        Name = name;
    }

    public GroupResult(IEnumerable<Customer> customers)
    {
        Name = Guid.NewGuid().ToString();
        NumRecords = customers.Count();
    }

    public GroupResult(string name, IEnumerable<Customer> customers)
    {
        Name = name;
        NumRecords = customers.Count();
    }

}

The main static class that displays prompts to select column to group on, creates the relevant expression tree and executes it

internal static class SimpleGroupByCustomer
{
    internal static DataContext db;

    internal static void Execute()
    {
        using (db = new DataContext())
        {
            //get input
            Console.WriteLine();
            Console.WriteLine("Simple Customer GroupBy");
            Console.WriteLine("=======================");
            Console.WriteLine("Simple GroupBy on the Customer Table");
            Console.WriteLine();

            Console.WriteLine("Select the property that you want to group by.");
            Console.WriteLine();

            var dbSet = db.Set<Customer>();
            var query = dbSet.AsQueryable();

            //for this example we're just prompting for a column in the customer table
            //GetColumnName is a helper function that lists the available columns and allows
            //one to be selected
            string colName = Wrapper.GetColumnName("Customer");

            MethodInfo? method = typeof(SimpleGroupByCustomer).GetMethod("GetGroupBy",
                BindingFlags.Static | BindingFlags.NonPublic);

            if (method != null)
            {
                method = method.MakeGenericMethod(new Type[] { typeof(String), query.ElementType });
                method.Invoke(null, new object[] { query, colName });
            }
        }
    }

    internal static void GetGroupBy<T, TTable>(IQueryable query, string colName)
    {
        Type TTmp = typeof(TTable);
        var param = Expression.Parameter(TTmp, "c");
        var prop = Expression.PropertyOrField(param, colName);

        LambdaExpression keySelector = Expression.Lambda<Func<TTable, T>>(prop, param);

        var param1 = Expression.Parameter(typeof(T), "Key");
        var param2 = Expression.Parameter(typeof(IEnumerable<TTable>), "Customers");

        var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T), typeof(IEnumerable<TTable>) });
        //var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T) });
        //var ci = typeof(GroupResult).GetConstructor(new[] { typeof(IEnumerable<TTable>) });

        if (ci == null)
            return;

        var pExp = new ParameterExpression[] { param1, param2 };

        var methodExpression = Expression.Lambda<Func<T, IEnumerable<TTable>, GroupResult>>(
            Expression.New(ci, new Expression[] { param1, param2 }), //<--- ERROR HERE
            pExp
            );


        Type[] typeArgs = new Type[] { typeof(TTable), typeof(T), typeof(GroupResult) };
        Expression[] methodParams = new Expression[] { query.Expression, keySelector, methodExpression };

        var resultExpression = Expression.Call(typeof(Queryable), "GroupBy", typeArgs, methodParams);

        IQueryable dbQuery = query.Provider.CreateQuery(resultExpression);

        if (dbQuery is IQueryable<GroupResult> results)
        {
            foreach (var result in results)
            {
                Console.WriteLine("{0,-15}\t{1}", result.Name, result.NumRecords.ToString());
            }
        }
    }
}

When I run this and try and iterate through the results I get the following exception:

> System.InvalidOperationException: 'variable 'Customers' of type 'System.Collections.Generic.IEnumerable`1[ExpressionTrees3.Data.Customer]' referenced from scope '', but it is not defined'

which is being caused by the param2 ParameterExpression marked above.

If I use the GroupResult constructor that just takes the key value

var ci = typeof(GroupResult).GetConstructor(new[] { typeof(T) });

and omit the param2 from the Lambda body definition the code works as expected and I get a collection of GroupResult records containing the distinct key values in the Name field (but obviously no summary value).

I've tried everything I can think of and just can't get past this error - it's as though the GroupBy is not actually producing the IEnumerable grouping of Customers for each key.

I suspect I'm missing something really obvious here, but just can't see it. Any help would really very much appreciated.

Please note that I am after answers to this specific issue, I'm not looking for alternative ways of doing a GroupBy (unless there's a fundamental reason why this shouldn't work) - this will be rolled into a much larger solution for building queries and I want to use the same process throughout.

答案1

得分: 1

谢谢Svyatoslav - 正如我所想,是我特别愚蠢!

您的评论以及与一个拥有很多SQL知识的朋友的讨论指引我走上了正确的方向。

我一直以为GroupBy表达式会返回每个键值的可枚举对象,并试图将其传递给一个函数...这总是感觉不对,但我只是忽略了这一点并继续前进。

现在显而易见的是,我需要告诉GroupBy要计算和返回什么(即您提到的聚合)。

所以对于这个简单的示例,解决方案非常简单:

var pExp = new ParameterExpression[] { param1, param2 };

var countTypes = new Type[] { typeof(TTable) };
var countParams = new Expression[] { param2 };
var countExp = Expression.Call(typeof(Enumerable), "Count", countTypes, countParams);

var methodExpression = Expression.Lambda<Func<T, IEnumerable<TTable>, GroupResult>>(
    Expression.New(ci, new Expression[] { param1, countExp }),
    pExp
    );

只需在GroupBy方法调用中添加'Count'表达式,它就可以工作了!

.. 并添加一个新的GroupResult构造函数:

public GroupResult(string name, int count)
{
    Name = name;
    NumRecords = count;
}

(是的,我感觉有点愚蠢!)

英文:

Thanks Svyatoslav - as I thought, it was me being especially dumb!

Your comments, as well as a discussion with a friend who has a lot SQL knowledge pointed me in the right direction.

I had been thinking that the GroupBy expression was going to return an Enumerable for each key value and was trying to pass that into a function ... it always felt wrong, but I just ignored that and kept going.

It's obvious now that I need to tell the GroupBy what to calculate and return (i.e. your comment about aggregation).

So for this easy example, the solution is very simple:

        var pExp = new ParameterExpression[] { param1, param2 };
var countTypes = new Type[] { typeof(TTable) };
var countParams = new Expression[] { param2 };
var countExp = Expression.Call(typeof(Enumerable), &quot;Count&quot;, countTypes, countParams);
var methodExpression = Expression.Lambda&lt;Func&lt;T, IEnumerable&lt;TTable&gt;, GroupResult&gt;&gt;(
Expression.New(ci, new Expression[] { param1, countExp }),
pExp
);

Just by adding the 'Count' expression into the GroupBy method call it works!

.. and adding a new ctor for GroupResult:

public GroupResult(string name, int count)
{
Name = name;
NumRecords = count;
}

(yep, I feel a bit stupid!)

huangapple
  • 本文由 发表于 2023年1月9日 01:52:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75050103.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定