C#使用表达式树生成分组后求和的select语句

Using an expression tree to generate a select statement with a sum after a group by C#

如果问题没有真正意义,我们深表歉意,

我们在某些上下文中使用 C#、Entity Framework 和 Linq。

所以问题是:

用户可以 select 对多个字段进行汇总,例如数据集中的总值、净值、增值税,然后当 运行 对 return 数据的标准查询时,查询必须汇总这些列以供搜索。

我正在尝试将这些语句转换为动态表达式树。

List<string> propFrom = new List<string>()
{
   "Class1.PropertyName",
   "Class1.AnotherPropertyName"
}

Expression<Func<Grouped<Class1>, Class2>> select = (x => new Class2()
{
   Class1Prop = x.Sum(s => s.Class2Prop),
   Class1AntoherProp = x.Sum(s => s.Class2AnotherProp)
})

Class1 上的属性与 Class2 相匹配,只有字符串列表中的属性将被求和并 selected 到 select 列表中。

我了解如何为标准 select 语句生成表达式树,而不是如何在分组后使用求和来生成表达式树。

如果有人可以考虑如何改写问题或对答案有任何帮助,我们将不胜感激。

-- 更多细节 --

应用程序显示 table 发票数据

Table 看起来像这样

invoiceNo, 总的, 增值税, 网络....其他领域

table 带回所有行,用户还可以说我希望汇总任何字段,例如我想总结所有增值税和/或所有净额,或者只是总计,问题是我们不知道他们想要汇总哪些字段,如果我们这样做我们可以做到

query.Sum(x => x.net)

但我们有多个未知列的总和,因此我们尝试使用 属性 名称映射到 select 语句,如上所示

数据示例,

invoices      Net      Gross    Vat
1             10       10       10
2             20       20       20
3             10       30       30
4             15       40       40
5             50       50       50
6             5        60       60

用户在此处指定总毛额和增值税以获得 毛 - 210,增值税 - 210 除了他们所有的结果

是的,查询按 1 分组以获得聚合来计算总和,但那是在例如

之前完成的
query.groupBy(x => 1).select(insertDynamicSelectHere);

这是您想要做的吗?

public class Data
{
    public int Value1 { get; set; }
    public int Value2 { get; set; }
    public int Value3 { get; set; }
}

public class DataContainer
{
    public IQueryable<Data> Data { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var list = MakeList();

        // Hardcoded
        var sumExpression1 = (Expression<Func<Data, Int32>>)(d => d.Value1); 

        // Generate property expression
        var sumExpression2 = GeneratePropertyExpression<Data>("Value2"); // THIS?

        // Generate Sum expression
        var sumExpression3 = GenerateSumExpression<DataContainer, Data>("Value3"); // OR THIS? 

        var selected = list.Select(c => new
        {
            Sum1 = c.Data.Sum(sumExpression1),
            Sum2 = c.Data.Sum(sumExpression2),

            // will not need the compile() if you generate the entire select expression.
            Sum3 = sumExpression3.Compile()(c)  
        }); ;

        var first = selected.First();
        var last = selected.Last();

        Console.WriteLine($"First().Sum1 = {first.Sum1}"); // 2
        Console.WriteLine($"Last().Sum2 = {last.Sum2}");   // 6
        Console.WriteLine($"Last().Sum3 = {last.Sum3}");   // 9 
    }

    private static Expression<Func<TParent, Int32>> GenerateSumExpression<TParent, TChild>(String field)
    {
        var parameter = Expression.Parameter(typeof(TParent), "c");

        // I don't know a cleaner way to get the LINQ methods than this. 
        // We use Enumerable because it wants a concrete type.
        var sumMethod =
        (
            from m in typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
            where m.Name == "Sum"
            let p = m.GetParameters()
            where p.Length == 2
                && p[0].ParameterType.IsGenericType
                && p[0].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
                && p[1].ParameterType.IsGenericType
                && p[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)
            select m
        ).FirstOrDefault()
         .MakeGenericMethod(typeof(TChild)); // Enumerable is not generic, so we'll make it a generic with this reflection method. 

        var nestedLambda = GeneratePropertyExpression<TChild>(field);

        // 'parameter' is our DataContainer object, while 'Data' is the
        // property we want. 
        var prop = Expression.PropertyOrField(parameter, "Data");

        // Null is the first parameter because it's a static extension method.
        var body = Expression.Call(null, sumMethod, prop, nestedLambda);
        return Expression.Lambda<Func<TParent, Int32>>(body, parameter);
    }

    private static Expression<Func<T, Int32>> GeneratePropertyExpression<T>(String field)
    {
        var parameter = Expression.Parameter(typeof(T), "d");
        var body = Expression.PropertyOrField(parameter, field);
        return Expression.Lambda<Func<T, Int32>>(body, parameter);
    }

    public static IQueryable<DataContainer> MakeList()
    {
        return new List<DataContainer>()
        {
            new DataContainer()
            {
                Data = new List<Data>()
                {
                    new Data() { Value1 = 1, Value2 = 2, Value3 = 3 },
                    new Data() { Value1 = 1, Value2 = 2, Value3 = 3 }
                }.AsQueryable()
            },
            new DataContainer()
            {
                Data = new List<Data>()
                {
                    new Data() { Value1 = 1, Value2 = 2, Value3 = 3  },
                    new Data() { Value1 = 1, Value2 = 2, Value3 = 3  },
                    new Data() { Value1 = 1, Value2 = 2, Value3 = 3  }
                }.AsQueryable()
            }
        }.AsQueryable();
    }

如果我没看错你的要求,这里是代码:

public static class LinqExtensions
{
    public static Expression<Func<IGrouping<int, TInput>, TOutput>> AggregateExpression<TInput, TOutput>(string[] strings) where TInput: new()
    {
        ParameterExpression p = Expression.Parameter(typeof(IGrouping<int, TInput>));

        // Create object using Member Initialization; for example `new XXX { A = a.Sum(b => b.A), B = a.Sum(b => b.B) }`
        MemberInitExpression body = Expression.MemberInit(
            Expression.New(typeof(TOutput).GetConstructor(Type.EmptyTypes)),
            strings.Select(CreateMemberAssignment).ToArray()
        );

        // Create lambda
        return Expression.Lambda<Func<IGrouping<int, TInput>, TOutput>>(body, p);

        // Create single member assignment for MemberInit call
        // For example for expression `new XXX { A = a.Sum(b => b.A), B = a.Sum(b => b.B) }` it can be `A = a.Sum(b => b.A)` or `B = a.Sum(b => b.B)`
        MemberAssignment CreateMemberAssignment(string prop)
        {
            // If needed you can map TInput.Prop to TOutput.Prop names here
            PropertyInfo propInfo = typeof(TOutput).GetProperty(prop);

            return Expression.Bind(
                propInfo,
                Expression.Convert(
                    Expression.Call(
                        typeof(Enumerable),
                        "Sum",
                        new[] {typeof(TInput)},
                        new[] {p, CreateSumLambda(prop)}
                    ),
                    propInfo.PropertyType
                )
            );
        }

        // Create Lambda to be passed to Sum method
        Expression CreateSumLambda(string prop)
        {
            ParameterExpression q = Expression.Parameter(typeof(TInput));
            return Expression.Lambda(Expression.Property(q, prop), q);
        }
    }
}

所以不用调用

invoices.GroupBy(x => 1)
    .AsQueryable()
    .Select(i => new AggregatedInvoice
    {
        Net = i.Sum(x => x.Net),
        Gross = i.Sum(x => x.Gross)
    })

可以打电话

invoices.GroupBy(x => 1)
    .AsQueryable()
    .Select(LinqExtensions.AggregateExpression<Invoice, AggregatedInvoice>(new[] { "Net", "Gross" }));

对于以下型号:

public class Invoice
{
    public int Id { get; set; }
    public decimal Net { get; set; }
    public decimal Gross { get; set; }
    public decimal Vat { get; set; }
}

public class AggregatedInvoice
{
    public decimal? Net { get; set; }
    public decimal? Gross { get; set; }
    public decimal? Vat { get; set; }
}

方法接受 2 个类型参数 TInputTOutput。如果需要,您可以对两者使用相同的 class。 唯一的限制是 TInputTOutput 必须具有相同名称的属性。