ineffable p
ineffable p

Reputation: 1189

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

Upvotes: 15

Views: 30702

Answers (5)

Adriano Repetti
Adriano Repetti

Reputation: 67080

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.

Upvotes: 25

ineffable p
ineffable p

Reputation: 1189

You solution is simple to implement for any model, I have just made it generic one.

public static Expression<Func<TElement, string>> GetColumnName<TElement>(string property)
    {
        var menu = Expression.Parameter(typeof(TElement), "groupCol");
        var menuProperty = Expression.PropertyOrField(menu, property);
        var lambda = Expression.Lambda<Func<TElement, string>>(menuProperty, menu);
        return lambda;
    }

so called this as below

_unitOfWork.MenuRepository.Get().GroupBy(LinqExtensions.GetColumnName<Menu>("MenuText").Compile());

Thank you very much for the help.

Upvotes: 0

ineffable p
ineffable p

Reputation: 1189

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

Upvotes: 2

Dustin Kingen
Dustin Kingen

Reputation: 21245

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.

Upvotes: 5

Daniel Hilgarth
Daniel Hilgarth

Reputation: 174289

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.

Upvotes: 8

Related Questions