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 个类型参数 TInput
和 TOutput
。如果需要,您可以对两者使用相同的 class。
唯一的限制是 TInput
和 TOutput
必须具有相同名称的属性。
如果问题没有真正意义,我们深表歉意,
我们在某些上下文中使用 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 个类型参数 TInput
和 TOutput
。如果需要,您可以对两者使用相同的 class。
唯一的限制是 TInput
和 TOutput
必须具有相同名称的属性。