动态LINQ将IQueryable聚合为单个查询

时间:2014-02-07 17:59:06

标签: c# linq dynamic-linq

我正在构建一个适用于通用IQueryable数据源的数据网格库。在底部选定的列将具有聚合:总和,平均值,计数等。

我可以使用本文How to do a Sum using Dynamic LINQ

中的代码单独计算总和/平均值/计数

我不想为数据源单独运行它们,因为这会导致对数据库进行多次查询,我宁愿创建一个表达式树,并将其作为单个查询执行。

在静态LINQ中,您将执行所有.Sum,.Average和.Count方法,并返回带有值的新匿名类型。我不需要匿名类型(除非这是唯一的方法):聚合的列表或数组都没问题。

我假设从另一篇文章中我需要以某种方式将一系列MethodCallExpression对象串起来。有人可以帮忙吗?

3 个答案:

答案 0 :(得分:2)

我找到了一种使用Dynamic LINQ库的替代方法,避免了构造复杂的表达式树。

对于任何感兴趣的人,解决方案都在下面的单元测试中。我有一个名为TestQueryableDataset的随机数据集。此IQueryable数据源的泛型类型具有Total属性(十进制),Discount属性(可为空的十进制)和ID属性(int)。

单元测试首先使用静态LINQ查询获得预期结果。

然后构造一个select语句,该语句使用groupby变量'it'来计算总和,平均值和计数。属性名称由字符串传递,以证明这是字符串类型。

分组方法 .GroupBy(x => 1)是一个虚拟分组,可以将聚合应用于整个数据集。

请注意,这将返回具有属性t0,t1和t2的单个动态结果。但是,groupby / select操作仍然返回IQueryable但只有一个结果。我们必须使用t.Cast()。First();转换为对象数组,然后得到第一个结果。

然后我们可以使用反射来获取每个结果的属性(t0,t1,t2)作为实际值,并断言它们与我们之前得到的静态结果相匹配。

    [TestMethod()]
    [TestProperty("Anvil.DataSets", "QueryableExtensions")]
    public void DynamicAggregate_test()
    {
        var source = new Anvil.Test.DataSets.TestQueryableDataset();

        var data = source.GetData();

        var expectedTotal = (from d in data select d.Total).Sum();
        var expectedDiscount = (from d in data select d.Discount).Average();
        var expectedCount = (from d in data select d.ID).Count();

        const string prop0 = "Total";
        const string prop1 = "Discount";
        const string prop2 = "ID";

        string sumExpr = string.Format("new ( Sum(it.{0}) as t0, Average(it.{1}) as t1 , Count() as t2)", prop0,prop1, prop2);
        var t = data.GroupBy(x => 1).Select(sumExpr);

        var firstItem = t.Cast<object>().First();

        var ttype = firstItem.GetType();
        var p0 = ttype.GetProperty("t0");
        var p1 = ttype.GetProperty("t1");
        var p2 = ttype.GetProperty("t2");

        decimal actualTotal = (decimal)(p0.GetValue(firstItem));
        decimal actualDiscount = (decimal)(p1.GetValue(firstItem));
        int actualCount = (int)(p2.GetValue(firstItem));

        Assert.AreEqual(expectedTotal, actualTotal);
        Assert.AreEqual(expectedDiscount, actualDiscount);
        Assert.AreEqual(expectedCount, actualCount);
    }

另见:

答案 1 :(得分:2)

您不需要匿名类型。您只需要一个包含3个属性SumCountAverage的类型。设计时不知道SumAverage类型。因此,对这两个属性使用Object类型。 Count始终为int

public class Aggregation
{
    public Aggregation(object sum, object average, int count)
    {
        Sum = sum;
        Average = average;
        Count = count;
    }
    public object Sum { get; private set; }
    public object Average { get; private set; }
    public int Count { get; private set; }
}

与文章How to do a Sum using Dynamic LINQ中描述的Sum扩展方法类似,您可以编写Aggregate扩展方法,从Aggregation计算IQueryable类实例集合和属性名称。 真正的困难在于确定与属性类型匹配的平均重载方法。无法从返回类型确定重载,而是从用作第二个参数的lambda表达式的返回类型确定。

例如,如果属性类型是int,则代码必须选择public static double Average<TSource>( this IQueryable<TSource> source, Expression<Func<TSource, int>> selector )重载。

public static Aggregation Aggregate(this IQueryable source, string member)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (member == null)
        throw new ArgumentNullException("member");

    // Properties
    PropertyInfo property = source.ElementType.GetProperty(member);
    ParameterExpression parameter = Expression.Parameter(source.ElementType, "s");
    Expression selector = Expression.Lambda(Expression.MakeMemberAccess(parameter, property), parameter);
    // We've tried to find an expression of the type Expression<Func<TSource, TAcc>>,
    // which is expressed as ( (TSource s) => s.Price );

    // Methods
    MethodInfo sumMethod = typeof(Queryable).GetMethods().First(
        m => m.Name == "Sum"
            && m.ReturnType == property.PropertyType // should match the type of the property
            && m.IsGenericMethod);
    MethodInfo averageMethod = typeof(Queryable).GetMethods().First(
        m => m.Name == "Average"
            && m.IsGenericMethod
            && m.GetParameters()[1]
                .ParameterType
                    .GetGenericArguments()[0]
                        .GetGenericArguments()[1] == property.PropertyType);
    MethodInfo countMethod = typeof(Queryable).GetMethods().First(
        m => m.Name == "Count"
            && m.IsGenericMethod);

    return new Aggregation(
        source.Provider.Execute(
            Expression.Call(
                null,
                sumMethod.MakeGenericMethod(new[] { source.ElementType }),
                new[] { source.Expression, Expression.Quote(selector) })),
        source.Provider.Execute(
            Expression.Call(
                null,
                averageMethod.MakeGenericMethod(new[] { source.ElementType }),
                new[] { source.Expression, Expression.Quote(selector) })),
        (int)source.Provider.Execute(
            Expression.Call(
                null,
                countMethod.MakeGenericMethod(new[] { source.ElementType }),
                new[] { source.Expression })));
}

答案 2 :(得分:0)

这是我的sum,average和min,max的解决方案..这是我在其中一个项目中使用的。

public static object AggregateFunc(this IQueryable source, string function, string member)
{
        if (source == null) throw new ArgumentNullException("source");
        if (member == null) throw new ArgumentNullException("member");

        // Properties
        PropertyInfo property = source.ElementType.GetProperty(member);
        ParameterExpression parameter = Expression.Parameter(source.ElementType, "s");

        // We've tried to find an expression of the type Expression<Func<TSource, TAcc>>,
        // which is expressed as ( (TSource s) => s.Price );

        Type propertyType = property.PropertyType;
        Type convertPropType = property.PropertyType;
        if (function == "Sum")//convert int to bigint
        {
            if (propertyType == typeof(Int32))
                convertPropType = typeof(Int64);
            else if (propertyType == typeof(Int32?))
                convertPropType = typeof(Int64?);
        }
        Expression selector = Expression.Lambda(Expression.Convert(Expression.MakeMemberAccess(parameter, property), convertPropType), parameter);
        //var methods = typeof(Queryable).GetMethods().Where(x => x.Name == function);
        // Method
        MethodInfo aggregateMethod = typeof(Queryable).GetMethods().SingleOrDefault(
            m => m.Name == function
                && m.IsGenericMethod
                && m.GetParameters().Length == 2 && m.GetParameters()[1].ParameterType.GenericTypeArguments[0].GenericTypeArguments[1] == convertPropType);// very hacky but works :)

        MethodCallExpression callExpr;
        // Sum, Average
        if (aggregateMethod != null)
        {
            callExpr = Expression.Call(
                    null,
                    aggregateMethod.MakeGenericMethod(new[] { source.ElementType }),
                    new[] { source.Expression, Expression.Quote(selector) });
            return source.Provider.Execute(callExpr);
        }
        // Min, Max
        else
        {
            aggregateMethod = typeof(Queryable).GetMethods().SingleOrDefault(
                m => m.Name == function
                    && m.GetGenericArguments().Length == 2
                    && m.IsGenericMethod);
            if (aggregateMethod != null)
            {
                callExpr = Expression.Call(
                    null,
                    aggregateMethod.MakeGenericMethod(new[] { source.ElementType, propertyType }),
                    new[] { source.Expression, Expression.Quote(selector) });

                return source.Provider.Execute(callExpr);
            }                
        }
        return null;
    }