我有一个包含大量属性的类,我需要按几乎所有列对其进行分组。
class Sample {
public string S1 { get; set; }
public string S2 { get; set; }
public string S3 { get; set; }
public string S4 { get; set; }
// ... all the way to this:
public string S99 { get; set; }
public decimal? N1 { get; set; }
public decimal? N2 { get; set; }
public decimal? N3 { get; set; }
public decimal? N4 { get; set; }
// ... all the way to this:
public decimal? N99 { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
有时我需要按除一两个十进制列之外的所有列进行分组,并基于此返回一些结果(即具有所有字段的对象,但带有一些十进制值作为总和或最大值)。
是否有任何扩展方法可以让我做这样的事情:
sampleCollection.GroupByExcept(x => x.N2, x => x.N5).Select(....);
Run Code Online (Sandbox Code Playgroud)
而不是指定对象中的所有列?
您找不到任何内置的东西可以处理这种情况。您必须自己创建一个。根据您需要的稳健程度,您可以采取多种方法。
您将遇到的主要障碍是如何生成密钥类型。在理想情况下,生成的新密钥将具有自己独特的类型。但它必须是动态生成的。
或者,您可以使用另一种类型,它可以保存多个不同的值,并且仍然可以适当地用作键。这里的问题是它仍然必须动态生成,但您将使用现有类型。
您可以采取的另一种不涉及生成新类型的方法是使用现有的源类型,但将排除的属性重置为其默认值(或根本不设置它们)。那么它们就不会对分组产生任何影响。这假设您可以创建此类型的实例并修改其值。
public static class Extensions
{
public static IQueryable<IGrouping<TSource, TSource>> GroupByExcept<TSource, TXKey>(this IQueryable<TSource> source, Expression<Func<TSource, TXKey>> exceptKeySelector) =>
GroupByExcept(source, exceptKeySelector, s => s);
public static IQueryable<IGrouping<TSource, TElement>> GroupByExcept<TSource, TXKey, TElement>(this IQueryable<TSource> source, Expression<Func<TSource, TXKey>> exceptKeySelector, Expression<Func<TSource, TElement>> elementSelector)
{
return source.GroupBy(BuildKeySelector(), elementSelector);
Expression<Func<TSource, TSource>> BuildKeySelector()
{
var exclude = typeof(TXKey).GetProperties()
.Select(p => (p.PropertyType, p.Name))
.ToHashSet();
var itemExpr = Expression.Parameter(typeof(TSource));
var keyExpr = Expression.MemberInit(
Expression.New(typeof(TSource).GetConstructor(Type.EmptyTypes)),
from p in typeof(TSource).GetProperties()
where !exclude.Contains((p.PropertyType, p.Name))
select Expression.Bind(p, Expression.Property(itemExpr, p))
);
return Expression.Lambda<Func<TSource, TSource>>(keyExpr, itemExpr);
}
}
}
Run Code Online (Sandbox Code Playgroud)
然后要使用它,你可以这样做:
sampleCollection.GroupByExcept(x => new { x.N2, x.N5 })...
Run Code Online (Sandbox Code Playgroud)
但可惜的是,这种方法在正常情况下行不通。您将无法在查询中创建该类型的新实例(除非您使用 Linq to Objects)。
如果您使用 Roslyn,您可以根据需要生成该类型,然后使用该对象作为密钥。但这意味着您需要异步生成类型。因此,您可能希望将其与查询完全分开,只生成键选择器。
public static async Task<Expression<Func<TSource, object>>> BuildExceptKeySelectorAsync<TSource, TXKey>(Expression<Func<TSource, TXKey>> exceptKeySelector)
{
var exclude = typeof(TXKey).GetProperties()
.Select(p => (p.PropertyType, p.Name))
.ToHashSet();
var properties =
(from p in typeof(TSource).GetProperties()
where !exclude.Contains((p.PropertyType, p.Name))
select p).ToList();
var targetType = await CreateTypeWithPropertiesAsync(
properties.Select(p => (p.PropertyType, p.Name))
);
var itemExpr = Expression.Parameter(typeof(TSource));
var keyExpr = Expression.New(
targetType.GetConstructors().Single(),
properties.Select(p => Expression.Property(itemExpr, p)),
targetType.GetProperties()
);
return Expression.Lambda<Func<TSource, object>>(keyExpr, itemExpr);
async Task<Type> CreateTypeWithPropertiesAsync(IEnumerable<(Type type, string name)> properties) =>
(await CSharpScript.EvaluateAsync<object>(
AnonymousObjectCreationExpression(
SeparatedList(
properties.Select(p =>
AnonymousObjectMemberDeclarator(
NameEquals(p.name),
DefaultExpression(ParseTypeName(p.type.FullName))
)
)
)
).ToFullString()
)).GetType();
}
Run Code Online (Sandbox Code Playgroud)
要使用这个:
sampleCollection.GroupBy(
await BuildExceptKeySelector((CollectionType x) => new { x.N2, x.N5 })
).Select(....);
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1374 次 |
| 最近记录: |