Home > Software engineering >  Can I create a generic GroupBy() in C# LINQ?
Can I create a generic GroupBy() in C# LINQ?

Time:10-08

I am trying to modify the a generic IQueryable extension method to perform a GroupBy operation. The method I have performs a generic Where on a dynamically defined column:

        public static IQueryable<TEntity> WhereById<TEntity, TKey>(
                       this IQueryable<TEntity> query, TKey value, string colName)
            where TEntity : class
        {
            var param = Expression.Parameter(typeof(TEntity), "e");
            var propAccess = Expression.PropertyOrField(param, colName);
            var valExpr = Expression.Constant(value);
   
            BinaryExpression predicate;
            predicate = Expression.Equal(propAccess, valExpr);
            var predicateLambda = Expression.Lambda<Func<TEntity, bool>>(predicate, param);
            return query.Where(predicateLambda);
        }

This works perfectly as in:

IQueryable<TEntity> entities = _crudApiDbContext.Set<TEntity>()
                    .WhereById<TEntity, int>(id, selectField);

Now I need a generic GroupBy(). I am trying the following:

        public static IQueryable<TEntity> GroupBy<TEntity>(this IQueryable<TEntity> query, string colName)
            where TEntity : class
        {
            var param = Expression.Parameter(typeof(TEntity), "e");
            var propAccess = Expression.PropertyOrField(param, colName);

            BinaryExpression predicate;
            predicate = Expression.XXX(propAccess);  <=what should XXX be?
            var predicateLambda = Expression.Lambda<Func<TEntity, int>>(propAccess);
            return (IQueryable<TEntity>)query.GroupBy(predicateLambda);
        }

and I guess my question is what should XXX be? Or maybe because GroupBy() is an extension method the approach needs to be different?

Update There is a lot of interest as to why I need this - I am building a generic Blazor form for faceted browsing. So I have a generic method for filtered and sorted search, but I also need to know for certain columns with options, how many options remain after applying the search conditions. To that end I will perform a GroupBy on each of those columns with the IQueryable which has the search conditions applied to it. Example of faceted search: enter image description here

CodePudding user response:

This is solution how to do grouping by dynamic field. Since we don't know type of grouping key, I've decided to make it as object.

public static class QueryableExtensions
{
    static Expression MakePropPath(Expression objExpression, string path)
    {
        return path.Split('.').Aggregate(objExpression, Expression.PropertyOrField);
    }

    public static IQueryable<IGrouping<object, TEntity>> GroupBy<TEntity>(this IQueryable<TEntity> query, string colName)
        where TEntity : class
    {
        var param      = Expression.Parameter(typeof(TEntity), "e");
        var propAccess = MakePropPath(param, colName);

        var keyLambda = Expression.Lambda(Expression.Convert(propAccess, typeof(object)), param);

        var groupCall = Expression.Call(typeof(Queryable), nameof(Queryable.GroupBy),
            new[] { typeof(TEntity), typeof(object) }, query.Expression,
            keyLambda);

        return query.Provider.CreateQuery<IGrouping<object, TEntity>>(groupCall);
    }
}

Usage is simple in your case:

var result = query.GroupBy("Genre")
    .Select(g => new 
    {
        g.Key,
        Count = g.Count()
    })
    .ToList();
  • Related