5

I am working on the application where user can select columns he/she wants to see on the screen and which columns to group by or aggregate. So, in my LINQ section I should actually pass variables that hold column names to both group by and aggregate clause. Keep in mind that DataTable dt may hold different data every time(e.g. Employee info, Purchase orders, Performance stats, etc). I can only get information about the data at run time via dt.Columns[i].ColumnName and dt.Columns[i].DataType.Name. Can any one advise how to do that, what I need is something like this:

        SqlDataAdapter da = new SqlDataAdapter(cmd);
        DataTable dt = new DataTable();
        da.Fill(dt);

        var query = from row in dt.AsEnumerable()
                    group row by new
                    {
                        foreach(DataColumn column in dt.Columns)
                        {
                           row[column.ColumnName];                          
                        }
                    } into grp

                    select new
                    {
                        foreach(DataColumn column in dt.Columns)
                        {
                           if(column.DataType.Name == "Decimal")
                           {
                             Sum(grp[column.ColumnName]);
                           }else{
                             grp[column.ColumnName];
                           }

                        }
                    };
superconsultant
  • 231
  • 2
  • 6
  • 20
  • possible duplicate of [LINQ Grouping dynamically](http://stackoverflow.com/questions/17678197/linq-grouping-dynamically) (also http://stackoverflow.com/questions/20329558/how-to-use-groupby-using-dynamic-linq and http://stackoverflow.com/questions/14758666/dynamic-grouping-using-linq and http://stackoverflow.com/questions/3929041/dynamic-linq-groupby-multiple-columns ) – hatchet - done with SOverflow Oct 30 '14 at 17:09
  • hatchet Thanks for trying, but I don't know names of the columns and number of columns at the time of codding, it always varies and available only at run time. I can get the column names from the DataTable only after it gets populated, but as just mentioned it could be populated with different data (meaning it could be product data at one time and employee info at another time) every time. – superconsultant Oct 30 '14 at 17:19
  • No luck, it still requires knowledge of the column names at the time of codding, which are not known. – superconsultant Oct 31 '14 at 15:45

2 Answers2

11

There are several ways to do this. Here's one.

Below is a class I use pretty often called NTuple. It is the same idea as the Tuple<T>, Tuple<T1, T2>, etc classes that come with the .NET framework. However, the NTuple class is designed to hold a variable number of items. Two NTuple instances are equal if they contain the same number of values and those values are equal.

Given a set of columns

// as per OP, the list of columns to group by will be generated at runtime
IEnumerable<string> columnsToGroupBy = ...; 

you can use the NTuple class to group by those columns like this:

var groups = dt.AsEnumerable()
    .GroupBy(r => new NTuple<object>(from column in columnsToGroupBy select r[column]));

Here's the beef:

public class NTuple<T> : IEquatable<NTuple<T>>
{
    public NTuple(IEnumerable<T> values)
    {
        Values = values.ToArray();
    }

    public readonly T[] Values;

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj))
            return true;
        if (obj == null)
            return false;
        return Equals(obj as NTuple<T>);
    }

    public bool Equals(NTuple<T> other)
    {
        if (ReferenceEquals(this, other))
            return true;
        if (other == null)
            return false;
        var length = Values.Length;
        if (length != other.Values.Length)
            return false;
        for (var i = 0; i < length; ++i)
            if (!Equals(Values[i], other.Values[i]))
                return false;
        return true;
    }

    public override int GetHashCode()
    {
        var hc = 17;
        foreach (var value in Values)
            hc = hc*37 + (!ReferenceEquals(value, null) ? value.GetHashCode() : 0);
        return hc;
    }
}

Here's a test case:

static void Main(string[] args)
{
    // some sample data
    var dt = new DataTable();
    dt.Columns.Add("NAME", typeof(string));
    dt.Columns.Add("CITY", typeof(string));
    dt.Columns.Add("STATE", typeof(string));
    dt.Columns.Add("VALUE", typeof(double));
    dt.Rows.Add("Mike", "Tallahassee", "FL", 3);
    dt.Rows.Add("Mike", "Tallahassee", "FL", 6);
    dt.Rows.Add("Steve", "Tallahassee", "FL", 5);
    dt.Rows.Add("Steve", "Tallahassee", "FL", 10);
    dt.Rows.Add("Steve", "Orlando", "FL", 7);
    dt.Rows.Add("Steve", "Orlando", "FL", 14);
    dt.Rows.Add("Mike", "Orlando", "NY", 11);
    dt.Rows.Add("Mike", "Orlando", "NY", 22);

    // some "configuration" data
    IEnumerable<string> columnsToGroupBy = new[] {"CITY", "STATE"};
    string columnToAggregate = "VALUE";

    // the test routine
    foreach (var group in dt.AsEnumerable().GroupBy(r => new NTuple<object>(from column in columnsToGroupBy select r[column])))
    {
        foreach (var keyValue in group.Key.Values)
        {
            Debug.Write(keyValue);
            Debug.Write(':');
        }
        Debug.WriteLine(group.Sum(r => Convert.ToDouble(r[columnToAggregate])));
    }
}
Michael Gunter
  • 12,528
  • 1
  • 24
  • 58
  • Michael, grouping works great, but in select section I am having a problem, in `foreach (var keyValue in group.Key.Values)` group.Key doesn't seem to have property Values. Any thoughts? – superconsultant Nov 03 '14 at 19:05
  • I just copied and pasted your code into button click event on my page and it doesn't recognize Values property of group.Key, I am using Visual Stidio 2013, 4.5 framework. Thanks for being patient with me. – superconsultant Nov 03 '14 at 19:45
  • Oh, crap! Sorry. I made a change to the NTuple class halfway through my post and forgot to update the post. I just fixed it. See above. – Michael Gunter Nov 03 '14 at 20:05
  • Michael, I am just trying your example, I have added it under update of original question. – superconsultant Nov 03 '14 at 20:12
  • Yes. Please note that I changed my answer. Also, please revert the change to your original post, as it muddles the original question and makes it hard for someone to determine what you're asking. – Michael Gunter Nov 03 '14 at 20:16
  • Works like a charm.Thanks alot Michael, this will get me going. – superconsultant Nov 03 '14 at 21:53
  • ``public class NTuple : IEquatable>`` what is this syntax? is this recursive or..?? – bh_earth0 May 22 '18 at 07:57
  • @blackholeearth0_gmail This is implementation of a generic interface (`IEquatable`) where the type parameter to the generic is the class itself. More simply, the pattern is `class Foo: IEquatable`. In this case, `Foo` is really an `NTuple`. The pattern `class C : I` is what's known as the [Curiously Recurring Template Pattern](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern). – Michael Gunter May 22 '18 at 15:36
  • Any idea how to do this when grouping on child models? For example: columnsToGroupBy = new[] { "Status.Name", "Impact.Name" } where Status and Impact are child models? It fails because it is using the column name as a string which doesn't exist in the parent model. – Matt Apr 27 '21 at 13:59
-2

A solution is to use the QueryByCube function provided by my component AdaptiveLINQ. Just build an Select expression using reflection . This expression is automatically converted by AdaptiveLINQ into a request using the GroupBy operator.

nlips
  • 1,258
  • 9
  • 25
  • 2
    Maybe I should not have downvoted, but since I am actually looking for a good answer to this problem, it just appeared to me that you really did not post any useful information, except to plug your own commercial product. – ACG Apr 29 '15 at 13:25