12

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()
    });
Tim Rogers
  • 21,297
  • 6
  • 52
  • 68
Shahab
  • 143
  • 1
  • 6
  • 1
    Why is your last piece of code not an actual piece of code, but instead something close to pseudo-code? Why do you want to define this list of keys separately? – BCdotWEB Apr 28 '15 at 09:25
  • @BCdotWEB my last piece of code is not an actual because for this code to work i need to build anonymous type at run-time from user specified keys. In my actual scenario i need to present my users a summary of sales w.r.t different parameters to satisfy questions like "How much sales the employees of specific designation have done" and "How much sales the employees of specific designation with specific discipline have done" etc. So the users select by which parameters they want to get summary by. – Shahab Apr 29 '15 at 03:32

5 Answers5

1

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:

Community
  • 1
  • 1
ken2k
  • 48,145
  • 10
  • 116
  • 176
1

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()
    })
Community
  • 1
  • 1
Tim Rogers
  • 21,297
  • 6
  • 52
  • 68
1

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

    }

}
jamespconnor
  • 1,382
  • 14
  • 29
0

Not sure if this is you wanted, but you could select all available keys as a new list and then join them.

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>();
    }
}
Greg
  • 1,076
  • 1
  • 9
  • 17
0

For my final solution to this problem, I used the coding approach from @jamespconnor 's answer but string as a grouping key could not help me much in my real scenario. So I used @tim-rogers 's basic idea of array as a grouping key and comparing the arrays using ArrayEqualityComparer.

To get the key properties specified by string collection I build a static class like

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

And changed @jamespconnor's GroupByKeys extension a little like

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's 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);
    }

}

Since I also needed to select the results in an anonymous type with each "Key" as its property and an additional "Total" property, but not being successful at it i ended up like

// 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;
                }   
            )
        );
Shahab
  • 143
  • 1
  • 6