.netobjectentity-framework-corefunctional-programminganonymous-types

How to create an anonymous object dynamically in .NET?


I have a .net 8 project with Entity Framework and I want to build a dynamic query for it.

User can define columns based by his choices using existing tables:

For example:

I have 3 tables: Divisions, Industries, Portfolios, and a fourth table EntryLine which is related to those 3.

In the UI user has a list of 3 columns which he could choose: Division, Industry, Portfolio. He can choose any combination of those 3, for example only 2 of them, only one etc.

Based on what user will choose I need to create this:

var entryLines = _context.EntryLines.AsQueryable();

Expression<Func<EntryLine, object>> func;

if(user choosed divizion){
    func.Append(x => new { DivisionName = x.EntryLine.Division.Name});

    // func = new {x.EntryLine.Division.Name};
}

if (user chose industry) {
     func.Append(x => new { IndustryName = x.EntryLine.Industry.Name });

    // if user did NOT choose industry - func = new { DivisionName = x.EntryLine.Division.Name};
    // if user chose industry - 
    // func = new { 
    // DivisionName = x.EntryLine.Division.Name
    // IndustryName = x.EntryLine.Industry.Name
    // };
}

After that call:

entryLines.GroupBy(func);

and so on..

I give you a simple example, but I have more columns, and a much much complex database, but the main idea is to create a single anonymous object based on what user chose in the UI and pass it to the GroupBy linq and also select the data and so on.


Solution

  • I found the solution with reflection and functional programing

    public static class ExpressionHelper
    {
        public static Func<Tin, object> BuildPredicate<Tin>(params string[] propertyNames)
        {
            var parameter = Expression.Parameter(typeof(Tin), "x");
            List<Expression> propertyExpressions = new List<Expression>();
    
            foreach (var propertyName in propertyNames)
            {
                PropertyInfo propertyInfo = typeof(Tin).GetProperty(propertyName);
                if (propertyInfo == null)
                {
                    throw new InvalidOperationException($"No property with name {propertyName} found.");
                }
    
                MemberExpression propertyAccess = Expression.PropertyOrField(parameter, propertyName);
                propertyExpressions.Add(Expression.Convert(propertyAccess, typeof(object)));
            }
    
            MethodInfo tupleCreateMethod = typeof(ValueTuple).GetMethods()
                .First(m => m.Name == "Create" && m.GetParameters().Length == propertyNames.Length);
            var genericTupleCreateMethod = tupleCreateMethod.MakeGenericMethod(propertyExpressions.Select(e => e.Type).ToArray());
            var tupleCreation = Expression.Call(genericTupleCreateMethod, propertyExpressions);
    
            var boxedTuple = Expression.Convert(tupleCreation, typeof(object));
    
            LambdaExpression lambda = Expression.Lambda(typeof(Func<Tin, object>), boxedTuple, parameter);
    
            return (Func<Tin, object>)lambda.Compile();
        }
    }
    

    the call will be:

    groupByColumns will be a list of strings like ["column1", column2 etc]

    var groupByPredicate = ExpressionHelper.BuildPredicate<EntryLineDetailedDto>(groupByColumns);
    
    var groupedEntryLines = entryLinesQuery.GroupBy(groupByPredicate)
                                            .Skip((pageNumber - 1) * pageSize)
                                            .Take(pageSize).ToList();