Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LINQ Grouping dynamically

I have a class list of records, so user can select to group rows dynamically by property name. For example MenuText, RoleName or ActionName. Then I have to execute grouping so I need a generic method to handle grouping by passing column name.

Example :

public class Menu
{
  public string MenuText {get;set;}
  public string RoleName {get;set;}
  public string ActionName {get;set;}
}

public class Menus 
{
 var list = new List<Menu>();
 list.Add( new Menu {MenuText="abc",RoleName ="Admin", ActionName="xyz"};
 list.Add( new Menu {MenuText="abc",RoleName ="Admin", ActionName="xyz"};
 list.Add( new Menu {MenuText="abc1",RoleName ="Admin1", ActionName="xyz1"};
 list.Add( new Menu {MenuText="abc1",RoleName ="Admin1", ActionName="xyz1"};

  /// columnName :- Name of the Menu class ( MenuText  or RoleName  or ActionName)

  public IEnumerable<IGrouping<string,IEnumerable<Menu>>> GroupMenu(string columnName)
  {
          // Here I want to get group the list of menu by the columnName 
  }
}
like image 807
ineffable p Avatar asked Jul 16 '13 13:07

ineffable p


4 Answers

If you're not working with a database you can use Reflection:

private static object GetPropertyValue(object obj, string propertyName)
{
    return obj.GetType().GetProperty(propertyName).GetValue(obj, null);
}

Used as:

var grouped = enumeration.GroupBy(x => GetPropertyValue(x, columnName));

This is a pretty raw solution, a better way should be to use Dynamic LINQ:

var grouped = enumeration.GroupBy(columnName, selector);

EDIT Dynamic LINQ maybe needs some explanations. It's not a technology, a library or a brand new framework. It's just a convenient name for a couple (2000 LOC) of helpers methods that let you write such queries. Just download their source (if you don't have VS samples installed) and use them in your code.

like image 74
Adriano Repetti Avatar answered Nov 08 '22 00:11

Adriano Repetti


The following approach would work with LINQ to Objects as well as with LINQ to EF / NHibernate / etc.
It creates an expression that corresponds to the column / property passed as the string and passes that expression to GroupBy:

private static Expression<Func<Menu, string>> GetGroupKey(string property)
{
    var parameter = Expression.Parameter(typeof(Menu));
    var body = Expression.Property(parameter, property);
    return Expression.Lambda<Func<Menu, string>>(body, parameter);
}

Usage with IQueryable<T> based data sources:

context.Menus.GroupBy(GetGroupKey(columnName));

Usage with IEnumerable<T> based data sources:

list.GroupBy(GetGroupKey(columnName).Compile());

BTW: The return type of your method should be IEnumerable<IGrouping<string, Menu>>, because an IGrouping<string, Menu> already means that there can be multiple Menu instances per key.

like image 33
Daniel Hilgarth Avatar answered Nov 07 '22 23:11

Daniel Hilgarth


Simplest way:

if(columnName == "MextText")
{
    return list.GroupBy(x => x.MenuText);
}

if(columnName == "RoleName")
{
    return list.GroupBy(x => x.RoleName);
}

if(columnName == "ActionName")
{
    return list.GroupBy(x => x.ActionName);
}

return list.GroupBy(x => x.MenuText);

You could also use expression trees.

private static Expression<Func<Menu, string>> GetColumnName(string property)
{
    var menu = Expression.Parameter(typeof(Menu), "menu");
    var menuProperty = Expression.PropertyOrField(menu, property);
    var lambda = Expression.Lambda<Func<Menu, string>>(menuProperty, menu);

    return lambda;
}

return list.GroupBy(GetColumnName(columnName).Compile());

This will produce a lambda menu => menu.<PropertyName>.

But there's not really much of a difference until the class gets bloated.

like image 5
Dustin Kingen Avatar answered Nov 07 '22 23:11

Dustin Kingen


I have done this with Dynamic Linq as suggested by Adriano

public static IEnumerable<IGrouping<object, TElement>> GroupByMany<TElement>(
        this IEnumerable<TElement> elements, params string[] groupSelectors)
    {
        var selectors = new List<Func<TElement, object>>(groupSelectors.Length);
        selectors.AddRange(groupSelectors.Select(selector => DynamicExpression.ParseLambda(typeof (TElement), typeof (object), selector)).Select(l => (Func<TElement, object>) l.Compile()));
        return elements.GroupByMany(selectors.ToArray());
    }

public static IEnumerable<IGrouping<object, TElement>> GroupByMany<TElement>(
        this IEnumerable<TElement> elements, params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            Func<TElement, object> selector = groupSelectors.First();
            return elements.GroupBy(selector);
        }
        return null;
    }
like image 2
ineffable p Avatar answered Nov 07 '22 23:11

ineffable p