Context:
Using Ag-Grid, users should be able to drag-drop columns they want to group on.

Let's say I have the following model and group by function:
List<OrderModel> orders = new List<OrderModel>()
{
new OrderModel()
{
OrderId = 184214,
Contact = new ContactModel()
{
ContactId = 1000
}
}
};
var queryOrders = orders.AsQueryable();
Edit: So people have made me realize that in below question, I was actually focusing on dynamically Select the correct items (which is one of the requirements), I missed out on actually doing the grouping. Therefore some edits have been made to reflect both issues: Grouping and selecting, strongly typed.
In a type-defined way:
Single column
IQueryable<OrderModel> resultQueryable = queryOrders
.GroupBy(x => x.ExclPrice)
.Select(x => new OrderModel() { ExclPrice = x.Key.ExclPrice});
Multiple columns
IQueryable<OrderModel> resultQueryable = queryOrders
.GroupBy(x => new OrderModel() { Contact = new ContactModel(){ ContactId = x.Contact.ContactId }, ExclPrice = x.ExclPrice})
.Select(x => new OrderModel() {Contact = new ContactModel() {ContactId = x.Key.Contact.ContactId}, ExclPrice = x.Key.ExclPrice});
However, the last one doesn't work, defining an OrderModel within the GroupBy apparently gives issues when translating it to SQL.
How do I build this GroupBy/ Select using Expressions?
Currently, I have got so far to select the correct items, but no grouping is done yet.
public static IQueryable<TModel> GroupByExpression(List<string> propertyNames, IQueryable<TModel> sequence)
{
var param = Expression.Parameter(typeof(TModel), "item");
var body = Expression.New(typeof(TModel).GetConstructors()[0]);
var bindings = new List<MemberAssignment>();
foreach (var property in propertyNames)
{
var fieldValue = typeof(TModel).GetProperty(property, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase);
var fieldValueOriginal = Expression.Property(param, fieldValue ?? throw new InvalidOperationException());
var memberAssignment = Expression.Bind(fieldValue, fieldValueOriginal);
bindings.Add(memberAssignment);
}
var result = sequence.Select(Expression.Lambda<Func<TModel, TModel>>(Expression.MemberInit(body, bindings), param));
return result;
}
This works fine until I want to introduce a relationship, so in my example, item.Contact.ContactId.
I have tried to do it this way:
public static IQueryable<TModel> GroupByExpression(List<string> propertyNames, IQueryable<TModel> sequence)
{
var param = Expression.Parameter(typeof(TModel), "item");
Expression propertyExp = param;
var body = Expression.New(typeof(TModel).GetConstructors()[0]);
var bindings = new List<MemberAssignment>();
foreach (var property in propertyNames)
{
if (property.Contains("."))
{
//support nested, relation grouping
string[] childProperties = property.Split('.');
var prop = typeof(TModel).GetProperty(childProperties[0], BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase);
propertyExp = Expression.MakeMemberAccess(param, prop);
//loop over the rest of the childs until we have reached the correct property
for (int i = 1; i < childProperties.Length; i++)
{
prop = prop.PropertyType.GetProperty(childProperties[i],
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase);
propertyExp = Expression.MakeMemberAccess(propertyExp, prop);
if (i == childProperties.Length - 1)//last item, this would be the grouping field item
{
var memberAssignment = Expression.Bind(prop, propertyExp);
bindings.Add(memberAssignment);
}
}
}
else
{
var fieldValue = typeof(TModel).GetProperty(property, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase);
var fieldValueOriginal = Expression.Property(param, fieldValue ?? throw new InvalidOperationException());
var memberAssignment = Expression.Bind(fieldValue, fieldValueOriginal);
bindings.Add(memberAssignment);
}
}
var memInitExpress = Expression.MemberInit(body, bindings);
var result = sequence.Select(Expression.Lambda<Func<TModel, TModel>>(memInitExpress, param));
return result;
}
Might look promising, but unfortunately, it throws an error at var memInitExpress = Expression.MemberInit(body, bindings);
ArgumentException ''ContactId' is not a member of type 'OrderModel''
So this is how the expression looks like when grouping on multiple columns:
Result of Expression.MemberInit(body, bindings) is:
{new OrderModel() {TotalInclPrice = item.TotalInclPrice, OrderId = item.OrderId}}
So the entire expression is: {item => new OrderModel() {TotalInclPrice = item.TotalInclPrice, OrderId = item.OrderId}}
So now it is not so difficult to understand why I get the exception I mentioned, simply because it is using the OrderModel to Select the properties, and ContactId is not in that model. However I am limited and required to stick to IQueryable<OrderModel>, so the question now is how to create the expression to group by ContactId using the same model. I would guess I would actually need to have a expression with this:
Result of Expression.MemberInit(body, bindings) would need to be:
{new OrderModel() { Contact = new ContactModel() { ContactId = item.Contact.ContactId} , OrderId = item.OrderId}}. Something like this?
So, I thought let's go back to the basics and do it step by step. Eventually, the for-loop creates the following expression. See my answer how I solve this part, Ivan's answer seems to have solved this in a generic way but I did not test that code yet. However, this does not do the grouping yet, so after applying grouping, these answers might not work anymore.
FYI: The AgGrid can find property relationships by just supplying the column field contact.contactId. So when the data is loaded, it just tries to find that property. I think when above expression is created, it would work within the Grid. I am trying myself now as well how to create sub-MemberInit's, because I think that is the solution in order to successfully do it.
If the idea is to create dynamically a nested MemberInit selector, it could be done as follows:
public static class QueryableExtensions
{
public static IQueryable<T> SelectMembers<T>(this IQueryable<T> source, IEnumerable<string> memberPaths)
{
var parameter = Expression.Parameter(typeof(T), "item");
var body = parameter.Select(memberPaths.Select(path => path.Split('.')));
var selector = Expression.Lambda<Func<T, T>>(body, parameter);
return source.Select(selector);
}
static Expression Select(this Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
{
var bindings = memberPaths
.Where(path => depth < path.Length)
.GroupBy(path => path[depth], (name, items) =>
{
var item = Expression.PropertyOrField(source, name);
return Expression.Bind(item.Member, item.Select(items, depth + 1));
}).ToList();
if (bindings.Count == 0) return source;
return Expression.MemberInit(Expression.New(source.Type), bindings);
}
}
Basically process member paths recursively, group each level by member name and bind the member to either source expression or MemberInit of source expression.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With