Linq 分组除列

dr4*_*ul4 5 c# linq grouping

我有一个包含大量属性的类,我需要按几乎所有列对其进行分组。

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)

而不是指定对象中的所有列?

Jef*_*ado 0

您找不到任何内置的东西可以处理这种情况。您必须自己创建一个。根据您需要的稳健程度,您可以采取多种方法。

您将遇到的主要障碍是如何生成密钥类型。在理想情况下,生成的新密钥将具有自己独特的类型。但它必须是动态生成的。

或者,您可以使用另一种类型,它可以保存多个不同的值,并且仍然可以适当地用作键。这里的问题是它仍然必须动态生成,但您将使用现有类型。

您可以采取的另一种不涉及生成新类型的方法是使用现有的源类型,但将排除的属性重置为其默认值(或根本不设置它们)。那么它们就不会对分组产生任何影响。这假设您可以创建此类型的实例并修改其值。

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)