Linq集团除了列

时间:2017-02-08 15:09:10

标签: 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; }
}

我不时需要按除除一个或两个十进制列之外的所有列进行分组,并根据此返回一些结果(即包含所有字段的对象,但将一些十进制值作为总和或最大值)。

是否有任何扩展方法可以让我做这样的事情:

sampleCollection.GroupByExcept(x => x.N2, x => x.N5).Select(....);

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

3 个答案:

答案 0 :(得分:0)

借鉴这个答案here:

创建课程EqualityComparer

public class EqualityComparer<T> : IEqualityComparer<T>
{
    public bool Equals(T x, T y)
    {
        IDictionary<string, object> xP = x as IDictionary<string, object>;
        IDictionary<string, object> yP = y as IDictionary<string, object>;

        if (xP.Count != yP.Count)
            return false;
        if (xP.Keys.Except(yP.Keys).Any())
            return false;
        if (yP.Keys.Except(xP.Keys).Any())
            return false;
        foreach (var pair in xP)
            if (pair.Value.Equals( yP[pair.Key])==false)
                return false;
        return true;

    }

    public int GetHashCode(T obj)
    {
        return obj.ToString().GetHashCode();
    }
}

然后创建GroupContent方法:

private void GroupContent<T>(List<T> dataList, string[] columns, string[] columnsToExclude)
    {
string[] columnsToGroup = columns.Except(columnsToExclude).ToArray();
        EqualityComparer<IDictionary<string, object>> equalityComparer = new EqualityComparer<IDictionary<string, object>>();
        var groupedList = dataList.GroupBy(x =>
        {
            var groupByColumns = new System.Dynamic.ExpandoObject();
            ((IDictionary<string, object>)groupByColumns).Clear();
            foreach (string column in columnsToGroup)
                ((IDictionary<string, object>)groupByColumns).Add(column, GetPropertyValue(x, column));
            return groupByColumns;
        }, equalityComparer);


        foreach (var item in groupedList)
        {
            Console.WriteLine("Group : " + string.Join(",", item.Key));
            foreach (object obj in item)
                Console.WriteLine("Item : " + obj);
            Console.WriteLine();
        }

    }

    private static object GetPropertyValue(object obj, string propertyName)
    {
        return obj.GetType().GetProperty(propertyName).GetValue(obj, null);
    }

答案 1 :(得分:0)

我将代码扩展到了借用另一个answer的上方。

public static class IEnumerableExt {
    public static IEnumerable<T> GroupBye<T, C>(this IEnumerable<T> query, Func<IGrouping<IDictionary<string, object>, T>, C> grouping) where T : class
    {
        var cProps = typeof(C).GetProperties().Select(prop => prop.Name).ToArray();
        var columnsToGroup = typeof(T).GetProperties().Select(prop => prop.Name).Except(cProps).ToArray();
        var equalityComparer = new EqualityComparer<IDictionary<string, object>>();
        return query
            .GroupBy(x => ExpandoGroupBy(x, columnsToGroup), equalityComparer)
            .Select(x => MergeIntoNew(x, grouping, cProps));
    }

    private static IDictionary<string, object> ExpandoGroupBy<T>(T x, string[] columnsToGroup) where T : class
    {
        var groupByColumns = new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
        groupByColumns.Clear();
        foreach (string column in columnsToGroup)
            groupByColumns.Add(column, typeof(T).GetProperty(column).GetValue(x, null));
        return groupByColumns;
    }

    private static T MergeIntoNew<T, C>(IGrouping<IDictionary<string, object>, T> x, Func<IGrouping<IDictionary<string, object>, T>, C> grouping, string[] cProps) where T : class
    {
        var tCtor = typeof(T).GetConstructors().Single();
        var tCtorParams = tCtor.GetParameters().Select(param => param.Name).ToArray();
        //Calling grouping lambda function
        var grouped = grouping(x);
        var paramsValues = tCtorParams.Select(p => cProps.Contains(p) ? typeof(C).GetProperty(p).GetValue(grouped, null) : x.Key[p]).ToArray();
        return (T)tCtor.Invoke(paramsValues);
    }

    private class EqualityComparer<T> : IEqualityComparer<T>
    {
        public bool Equals(T x, T y)
        {
            var xDict = x as IDictionary<string, object>;
            var yDict = y as IDictionary<string, object>;

            if (xDict.Count != yDict.Count)
                return false;
            if (xDict.Keys.Except(yDict.Keys).Any())
                return false;
            if (yDict.Keys.Except(xDict.Keys).Any())
                return false;
            foreach (var pair in xDict)
                if (pair.Value == null && yDict[pair.Key] == null)
                    continue;
                else if (pair.Value == null || !pair.Value.Equals(yDict[pair.Key]))
                    return false;
            return true;
        }

        public int GetHashCode(T obj)
        {
            return obj.ToString().GetHashCode();
        }
    }
}

可以通过以下方式使用:

var list = enumerable.GroupBye(grp => new
    {
        Value = grp.Sum(val => val.Value)
    });

结果类似于对除Value以外的所有其他列进行分组,其值将为分组元素的值之和

答案 2 :(得分: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);
        }
    }
}

然后使用它,您将执行以下操作:

sampleCollection.GroupByExcept(x => new { x.N2, x.N5 })...

但是,这种方法在正常情况下是行不通的。您将无法在查询中创建该类型的新实例(除非您使用的是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();
}

要使用此功能:

sampleCollection.GroupBy(
    await BuildExceptKeySelector((CollectionType x) => new { x.N2, x.N5 })
).Select(....);