IEnumerable Group. Пользовательский динамический список ключей

У меня есть класс вроде

public class Empolyee
{
    public string Designation {get ;set;}
    public string Discipline {get ;set;}
    public int Scale {get ;set;}
    public DateTime DOB {get ;set;}
    public int Sales {get ;set;}
}

и иметь записи всех сотрудников в переписном слове

List<Employee> Employees;

и список строковых ключей, например

var Keys = new List<string>()
{
    "Designation",
    "Scale",
    "DOB"
};

предположим, что элементы списка "Ключи" указаны пользователем, и пользователь может указать некие или многие ключевые элементы.

теперь я хочу Группировать всех "Сотрудников" с ключами, указанными в списке "Ключи" , и выбирать только свойства, указанные в "Ключах" плюс Сумма продаж для каждой группы.

из трех решений, которые я пытался использовать, после того, как он был применим, но не мог его использовать, потому что не знаю, как список "Ключи" будет преобразован в анонимный тип

Employees.GroupBy(e => new { e.Key1, e.Key2, ... })
    .Select(group => new {
        Key1 = group.Key.Key1,
        Key2 = group.Key.Key2,
        ...
        TotalSales = group.Select(employee => employee.Sales).Sum()
    });

Ответ 1

Для моего окончательного решения этой проблемы я использовал подход кодирования из ответа @jamespconnor, но строка в качестве ключа группировки не могла помочь мне в моем реальном сценарии. Поэтому я использовал основную идею @tim-rogers для массива в качестве ключа группировки и сравнения массивов с помощью ArrayEqualityComparer.

Чтобы получить свойства ключа, указанные в коллекции строк, я создаю статический класс, например

public static class MembersProvider
{
    public static IEnumerable<PropertyInfo> GetProperties(Type type, params string[] names)
    {
        var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
            .Where(pi => names.Contains(pi.Name))
            .Where(pi => pi != null)
            .AsEnumerable();
        if (names.Count() != properties.Count())
        {
            throw new InvalidOperationException("Couldn't find all properties on type " + type.Name);
        }

        return properties;
    }
}

И изменилось расширение @jamespconnor GroupByKeys немного похожее на

public static class GroupByExtensions
{
    public static IEnumerable<IGrouping<object[], TValue>> GroupByKeys<TValue>(this IEnumerable<TValue> values, IEnumerable<string> keys)
    {
        var properties = MembersProvider.GetProperties(typeof(TValue), keys.ToArray());
        var comparer = new ArrayEqualityComparer<object>();


        // jamespconnor string as key approch - off course it will need to return IEnumerable<IGrouping<string, TValue>> 
        /*return values.GroupBy(v => getters.Aggregate(
            "",
            (acc, getter) => string.Format(
                "{0}-{1}",
                acc,
                getter.Invoke(v, null).ToString()
                )
            )
        );*/

        //objects array as key approch 
        return values.GroupBy(v => properties.Select(property => property.GetValue(v, null)).ToArray(), comparer);
    }

}

Так как мне также нужно было выбирать результаты в анонимном типе с каждым "ключом" в качестве его свойства и дополнительным свойством "Всего", но не быть успешным в этом, я закончил, как

// get properties specified by "Keys" collection
    var properties = MembersProvider.GetProperties(typeof(Employee), Keys.ToArray());

    // Group and Select 
    var SalesSummary = Employees
        .GroupByKeys(Keys.ToArray())
        .Select(g =>
            properties.Aggregate(
                new Dictionary<string, object>() { { "TotalSales", g.Select(employee => employee.Sales).Sum() } },
                (dictionary, property) => {
                    dictionary.Add(property.Name, property.GetValue(g.FirstOrDefault(), null));
                    return dictionary;
                }   
            )
        );

Ответ 3

Если вы не знаете количество ключевых свойств upfront, статически скомпилированный анонимный тип не заставит вас очень далеко. Вместо этого вам понадобится массив для каждого группового ключа, так как число ключевых свойств является динамическим.

Сначала вам нужно сопоставить свои строки со значениями свойств:

public object[] MapProperty(string key, Employee e)
{
    switch (k) {
       case "Designation" : return e.Designation;
       case "DOB" : return e.Dob;
       // etc
    }
}

Затем вам придется группировать и сравнивать массивы, следя за тем, чтобы сравнить элементы каждого массива с помощью пользовательской реализации IEqualityComparer. Вы можете использовать ArrayEqualityComparer<T> из этого ответа.

var comparer = new ArrayEqualityComparer<object>();
Employees.GroupBy(e => Keys.Select(k => MapProperty(k, e)).ToArray(), e => e, comparer)
   .Select(group => new {
        Keys = group.Key,
        TotalSales = group.Select(employee => employee.Sales).Sum()
    })

Ответ 4

https://dotnetfiddle.net/jAg22Z

Это не особенно чисто, но может быть убрано - я просто использовал строку как ключ, так как она дает вам все хэш-код/​​равенство, которое требуется GroupBy, но вы могли бы создать класс для этого в более объектной путь.

Если вы действительно хотите сделать это со строками.

void Main()
{
        var vs = Enumerable.Range(0, 50).Select(i => Create(i));

        var groups = vs.GroupByKeys(new [] { "Scale" });

        Console.WriteLine("{0} groups", groups.Count());

        Console.WriteLine(string.Join(", ", groups.Select(g => g.Key)));

}
Employee Create(int i) {
    return new Employee { Scale = (((int)(i / 10)) * 10), DOB = new DateTime(2011, 11, 11), Sales = 50000 };

}
public class Employee
{
    public string Designation {get ;set;}
    public string Discipline {get ;set;}
    public int Scale {get ;set;}
    public DateTime DOB {get ;set;}
    public int Sales {get ;set;}
}

public static class GroupByExtensions 
{
    public static IEnumerable<IGrouping<string, TValue>> GroupByKeys<TValue>(this IEnumerable<TValue> values, IEnumerable<string> keys) 
    {
        var getters = typeof(TValue).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
            .Where(pi => keys.Contains(pi.Name))
            .Select(pi => pi.GetMethod)
            .Where(mi => mi != null)
            .ToArray();

        if (keys.Count() != getters.Length) 
        {
            throw new InvalidOperationException("Couldn't find all keys for grouping");
        }

        return values.GroupBy(v => getters.Aggregate("", (acc, getter) => string.Format("{0}¬{1}", acc, getter.Invoke(v, null).ToString())));

    }

}

Я бы посоветовал вам использовать функции для более сильной печати...

void Main()
{
        var vs = Enumerable.Range(0, 50).Select(i => Create(i));

        var groups = vs.GroupByKeys(new Func<Employee, object>[] { x=> x.Scale });

        Console.WriteLine("{0} groups", groups.Count());

        Console.WriteLine(string.Join(", ", groups.Select(g => g.Key)));

}
Employee Create(int i) {
    return new Employee { Scale = (((int)(i / 10)) * 10), DOB = new DateTime(2011, 11, 11), Sales = 50000 };

}
public class Employee
{
    public string Designation {get ;set;}
    public string Discipline {get ;set;}
    public int Scale {get ;set;}
    public DateTime DOB {get ;set;}
    public int Sales {get ;set;}
}

public static class GroupByExtensions 
{
    public static IEnumerable<IGrouping<string, TValue>> GroupByKeys<TValue>(this IEnumerable<TValue> values, IEnumerable<Func<TValue, object>> getters) 
    {

        return values.GroupBy(v => getters.Aggregate("", (acc, getter) => string.Format("{0}¬{1}", acc, getter(v).ToString())));

    }

}

Ответ 5

Не уверен, что это вам нужно, но вы можете выбрать все доступные ключи в качестве нового списка и затем присоединиться к ним.

void Main()
{
    var employees = new List<Employee>()
    {
        new Employee{
            Name = "Bob",
            Sales = 1,
            Keys = { "A", "B" }
        },
        new Employee{
            Name = "Jane",
            Sales = 2,
            Keys = { "A", "C" }
        }
    };

    var grouping = (from e in employees
            from k in employees.SelectMany(s => s.Keys).Distinct()
            where e.Keys.Contains(k)                        
            select new          
            {
                e.Name,
                e.Sales,
                Key = k         
            })
            .GroupBy(a => a.Key)
            .Select(g => new { Key = g.Key, TotalSales = g.Select(a => a.Sales).Sum() });           
}


public class Employee
{
    public int Sales { get; set; }
    public string Name { get; set; }
    public List<string> Keys { get; set;}

    public Employee()
    {
        Keys = new List<string>();
    }
}