Include more rows in Pivot

Viewed 357

I am using the extension method in below link to pivot my data: https://techbrij.com/pivot-c-array-datatable-convert-column-to-row-linq

I am including the code from the link just in case somebody finds this question in the future and the link is dead:

public static DataTable ToPivotTable<T, TColumn, TRow, TData>(
    this IEnumerable<T> source,
    Func<T, TColumn> columnSelector,
    Expression<Func<T, TRow>> rowSelector,
    Func<IEnumerable<T>, TData> dataSelector)
        {
            DataTable table = new DataTable();
            var rowName = ((MemberExpression)rowSelector.Body).Member.Name;
            table.Columns.Add(new DataColumn(rowName));
            var columns = source.Select(columnSelector).Distinct();

            foreach (var column in columns)
                table.Columns.Add(new DataColumn(column.ToString()));

            var rows = source.GroupBy(rowSelector.Compile())
                             .Select(rowGroup => new
                             {
                                 Key = rowGroup.Key,
                                 Values = columns.GroupJoin(
                                     rowGroup,
                                     c => c,
                                     r => columnSelector(r),
                                     (c, columnGroup) => dataSelector(columnGroup))
                             });

            foreach (var row in rows)
            {
                var dataRow = table.NewRow();
                var items = row.Values.Cast<object>().ToList();
                items.Insert(0, row.Key);
                dataRow.ItemArray = items.ToArray();
                table.Rows.Add(dataRow);
            }

            return table;
        }

Referring to the example in the link, you get the pivoted data like;

var pivotTable = data.ToPivotTable(
              item => item.Year, 
              item => item.Product,  
              items => items.Any() ? items.Sum(x=>x.Sales) : 0);

My question is, how can I include more rows into this query to return for example, ProductCode as well.. item => new {item.Product, item.ProductCode} does not work..


============== EDIT / 23 OCT 2018 ==============


Assuming my data is this;

enter image description here

With the help of the above mentioned code, I can manage to do this; enter image description here

What I want to achieve is this (extra col: STOCKID or any other cols as well); enter image description here

2 Answers

Example: https://dotnetfiddle.net/mXr9sh

The issue seems to be fetching the row names from the expression, since it's only designed to handle one row. That can be fixed by this function:

public static IEnumerable<string> GetMemberNames<T1, T2>(Expression<Func<T1, T2>> expression)
{
    var memberExpression = expression.Body as MemberExpression;
    if (memberExpression != null) 
    {
        return new[]{ memberExpression.Member.Name };
    }
    var memberInitExpression = expression.Body as MemberInitExpression;
    if (memberInitExpression != null)
    {
        return memberInitExpression.Bindings.Select(x => x.Member.Name);
    }
    var newExpression = expression.Body as NewExpression;
    if (newExpression != null)
    {
        return newExpression.Arguments.Select(x => (x as MemberExpression).Member.Name);
    }

    throw new ArgumentException("expression"); //use: `nameof(expression)` if C#6 or above
}

Once you have this function you can replace these lines:

var rowName = ((MemberExpression)rowSelector.Body).Member.Name;
table.Columns.Add(new DataColumn(rowName));

With this:

var rowNames = GetMemberNames(rowSelector);
rowNames.ToList().ForEach(x => table.Columns.Add(new DataColumn(x)));

One downside of this approach is the various values for these columns get returned concatenated in a single column; so you'll need to extract the data from the strings.


Resulting DataTable:

(displayed as JSON)

[
  {
    "StockId": "{ StockId = 65, Name = Milk }",
    "Name": "3",
    "Branch 1": "1",
    "Branch 2": "0",
    "Central Branch": null
  },
  {
    "StockId": "{ StockId = 67, Name = Coffee }",
    "Name": "0",
    "Branch 1": "0",
    "Branch 2": "22",
    "Central Branch": null
  }
]

Full Code Listing

using System;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
using Newtonsoft.Json; //just for displaying output

public class Program 
{
    public static void Main()
    {
        var data = new[] { 
            new { StockId = 65, Name = "Milk", Branch = 23, BranchName = "Branch 1", Stock = 3 },
            new { StockId = 65, Name = "Milk", Branch = 24, BranchName = "Branch 2", Stock = 1 },
            new { StockId = 67, Name = "Coffee", Branch = 22, BranchName = "Central Branch", Stock = 22 }
        };

        var pivotTable = data.ToPivotTable(
            item => item.BranchName, 
            item => new {item.StockId, item.Name},  
            items => items.Any() ? items.Sum(x=>x.Stock) : 0);

        //easy way to view our pivotTable if using linqPad or similar
        //Console.WriteLine(pivotTable);
        //if not using linqPad, convert to JSON for easy display
        Console.WriteLine(JsonConvert.SerializeObject(pivotTable, Formatting.Indented));
    }
}   

public static class PivotExtensions
{
    public static DataTable ToPivotTable<T, TColumn, TRow, TData>(
        this IEnumerable<T> source,
        Func<T, TColumn> columnSelector,
        Expression<Func<T, TRow>> rowSelector,
        Func<IEnumerable<T>, TData> dataSelector)
    {
        DataTable table = new DataTable();
        //foreach (var row in rowSelector()
        var rowNames = GetMemberNames(rowSelector);
        rowNames.ToList().ForEach(x => table.Columns.Add(new DataColumn(x)));
        var columns = source.Select(columnSelector).Distinct();

        foreach (var column in columns)
            table.Columns.Add(new DataColumn(column.ToString()));

        var rows = source.GroupBy(rowSelector.Compile())
            .Select(rowGroup => new
                    {
                        Key = rowGroup.Key,
                        Values = columns.GroupJoin(
                            rowGroup,
                            c => c,
                            r => columnSelector(r),
                            (c, columnGroup) => dataSelector(columnGroup))
                    });

        foreach (var row in rows)
        {
            var dataRow = table.NewRow();
            var items = row.Values.Cast<object>().ToList();
            items.Insert(0, row.Key);
            dataRow.ItemArray = items.ToArray();
            table.Rows.Add(dataRow);
        }

        return table;
    }
    public static IEnumerable<string> GetMemberNames<T1, T2>(Expression<Func<T1, T2>> expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        if (memberExpression != null) 
        {
            return new[]{ memberExpression.Member.Name };
        }
        var memberInitExpression = expression.Body as MemberInitExpression;
        if (memberInitExpression != null)
        {
            return memberInitExpression.Bindings.Select(x => x.Member.Name);
        }
        var newExpression = expression.Body as NewExpression;
        if (newExpression != null)
        {
            return newExpression.Arguments.Select(x => (x as MemberExpression).Member.Name);
        }

        throw new ArgumentException("expression"); //use: `nameof(expression)` if C#6 or above
    }

}

Anonymous types cannot be passed as generic parameters. Try defining your pivot key as a struct:

public struct PivotKey
{
    public string Product;
    public int ProductCode; // assuming your product codes are integers
}

This way you can take advantage of struct's default Equals and GetHashCode methods implementation in terms of the equality and hash code of all the fields.

Then, define the rowSelector as below:

item => new PivotKey { Product = item.Product, ProductCode = item.ProductCode}
Related