Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IEnumerable Group By user specified dynamic list of keys

I have a class like

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;}
}

and have records of all employees in an enumerable say

List<Employee> Employees;

and a list of string keys like

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

assume that elements of list "Keys" are user specified and user may specify no or many key elements.

now i want to Group all "Employees" with the keys specified in list "Keys" and select only the properties specified in "Keys" plus Sum of Sales for each group.

out of 3 solutions i tried to use, following looked applicable but could not use it because don't know how list "Keys" will be converted to anonymous type

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()
    });
like image 295
Shahab Avatar asked Apr 28 '15 09:04

Shahab


3 Answers

You probably need something like Dynamic LINQ so you can specify your keys and projected values as strings.

See some examples with grouping and projection:

  • How to use GroupBy using Dynamic LINQ
  • Dynamic LINQ GroupBy Multiple Columns
like image 61
ken2k Avatar answered Nov 07 '22 17:11

ken2k


Where you don't know the number of key properties upfront, a statically-compiled anonymous type isn't going to get you very far. Instead you will need an array for each group's key since the number of key properties is dynamic.

First you will need to map your strings to property values:

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

Then you will have to group and compare the arrays, making sure to compare the elements of each array using a custom IEqualityComparer implementation. You can use an ArrayEqualityComparer<T> from this answer.

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()
    })
like image 44
Tim Rogers Avatar answered Nov 07 '22 16:11

Tim Rogers


https://dotnetfiddle.net/jAg22Z

It's not particularly clean but could be tidied up - I've just used a string as the key since it gives you all the hashcode/equality that GroupBy needs but you could create a class to do this in a more object-friendly way.

If you really want to do it with strings.

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())));

    }

}

I'd encourage you to use functions for a little stronger typing...

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())));

    }

}
like image 1
jamespconnor Avatar answered Nov 07 '22 15:11

jamespconnor