构建关系分组表达式树c#
上下文: 使用Ag Grid,用户应该能够拖放他们想要分组的列 假设我有以下模型和功能分组:构建关系分组表达式树c#,c#,lambda,group-by,expression,expression-trees,C#,Lambda,Group By,Expression,Expression Trees,上下文: 使用Ag Grid,用户应该能够拖放他们想要分组的列 假设我有以下模型和功能分组: List<OrderModel> orders = new List<OrderModel>() { new OrderModel() { OrderId = 184214, Contact = new ContactModel() { ContactId = 1000 }
List<OrderModel> orders = new List<OrderModel>()
{
new OrderModel()
{
OrderId = 184214,
Contact = new ContactModel()
{
ContactId = 1000
}
}
};
var queryOrders = orders.AsQueryable();
多列
在我想引入一种关系之前,这种方法很有效,因此在我的示例中,item.Contact.ContactId
我尝试过这样做:
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;
}
公共静态IQueryable GroupByExpression(列表属性名称,IQueryable序列)
{
var param=表达式参数(typeof(TModel),“item”);
表达式propertyExp=param;
var body=Expression.New(typeof(TModel).GetConstructors()[0]);
var bindings=newlist();
foreach(propertyNames中的var属性)
{
if(property.Contains(“.”)
{
//支持嵌套、关系分组
字符串[]childProperties=property.Split('.');
var prop=typeof(TModel).GetProperty(childProperties[0],BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase);
propertyExp=Expression.MakeMemberAccess(param,prop);
//循环遍历其余的child,直到找到正确的属性
for(int i=1;i
可能看起来很有希望,但不幸的是,它在var memInitExpress=Expression.MemberInit(body,bindings)处抛出了一个错误代码>
ArgumentException“ContactId”不是“OrderModel”类型的成员
这就是在多个列上分组时表达式的外观:
Expression.MemberInit(body,bindings)
的结果是:
{new OrderModel(){TotalInclPrice=item.TotalInclPrice,OrderId=item.OrderId}}
因此,整个表达式是:{item=>newordermodel(){TotalInclPrice=item.TotalInclPrice,OrderId=item.OrderId}}
所以现在不难理解为什么我会遇到我提到的异常,因为它使用OrderModel
选择属性,而ContactId不在该模型中。但是,我受到限制,需要坚持使用IQueryable
,因此现在的问题是如何使用相同的模型创建表达式以通过ContactId
分组。我想我真的需要一个这样的表达:
Expression.MemberInit(body,bindings)的结果必须是:
{new OrderModel(){Contact=new ContactModel(){ContactId=item.Contact.ContactId},OrderId=item.OrderId}}
。像这样的
所以,我想让我们回到基础上来,一步一步地做。最后,for循环创建以下表达式。我是如何解决这一部分的,似乎已经以一种通用的方式解决了这个问题,但我还没有测试那个代码。但是,这还不能进行分组,因此在应用分组后,这些答案可能不再有效
仅供参考:AgGrid只需提供列字段
contact.contactId
,即可找到属性关系。因此,当加载数据时,它只会尝试查找该属性。我认为当创建上述表达式时,它将在网格中工作。我现在也在尝试如何创建sub-MemberInit
,因为我认为这是成功实现它的解决方案 这个答案有两个部分:
GroupBy
表达式,并确保使用相同的返回类型GroupBy
表达式的结果中创建Select
表达式键。
属性
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>();
var queryOrders = orders.AsQueryable();
var orderBindings = new List<MemberAssignment>();
//..more code was here, see question
var orderParam = Expression.Parameter(typeof(OrderModel), "item");
Expression orderPropertyExp = orderParam;
var orderPropContact = typeof(OrderModel).GetProperty("Contact");
orderPropertyExp = Expression.MakeMemberAccess(orderPropertyExp, orderPropContact);
var orderPropContactId = orderPropContact.PropertyType.GetProperty("ContactId");
orderPropertyExp = Expression.MakeMemberAccess(orderPropertyExp, orderPropContactId);
var contactBody = Expression.New(typeof(ContactModel).GetConstructors()[0]);
var contactMemerAssignment = Expression.Bind(orderPropContactId, propertyExp);
orderBindings.Add(contactMemerAssignment);
var contactMemberInit = Expression.MemberInit(Expression.New(contactBody, orderBindings);
var orderContactMemberAssignment = Expression.Bind(orderPropContact, contactMemberInit);
var orderMemberInit = Expression.MemberInit(Expression.New(typeof(OrderModel).GetConstructors()[0]), new List<MemberAssignment>() {orderContactMemberAssignment});
//during debugging with the same model, I know TModel is OrderModel, so I can cast it
//of course this is just a quick hack to verify it is working correctly in AgGrid, and it is!
return (IQueryable<TModel>)queryOrders.Select(Expression.Lambda<Func<OrderModel, OrderModel>>(orderMemberInit, param));
}
然后,我可以修改的Select
方法:
GetHashCode()
不幸的是,直到我开始在使用的每个模型中实现GetHasCode()
和Equals()
之前,这仍然不起作用。在Count()
或通过执行.ToList()
执行查询期间,它将比较所有对象,以确保对象彼此相等(或不相等)。如果它们相等:同一组。但由于我们动态生成了这些模型,因此无法根据内存位置(默认情况下)正确比较这些对象
幸运的是,您可以非常轻松地生成这两种方法:
确保表中至少包含您将使用的所有属性(并且可以按进行分组)。如果要动态创建嵌套的
MemberInit
选择器,请
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;
}
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;
}
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>();
var queryOrders = orders.AsQueryable();
var orderBindings = new List<MemberAssignment>();
//..more code was here, see question
var orderParam = Expression.Parameter(typeof(OrderModel), "item");
Expression orderPropertyExp = orderParam;
var orderPropContact = typeof(OrderModel).GetProperty("Contact");
orderPropertyExp = Expression.MakeMemberAccess(orderPropertyExp, orderPropContact);
var orderPropContactId = orderPropContact.PropertyType.GetProperty("ContactId");
orderPropertyExp = Expression.MakeMemberAccess(orderPropertyExp, orderPropContactId);
var contactBody = Expression.New(typeof(ContactModel).GetConstructors()[0]);
var contactMemerAssignment = Expression.Bind(orderPropContactId, propertyExp);
orderBindings.Add(contactMemerAssignment);
var contactMemberInit = Expression.MemberInit(Expression.New(contactBody, orderBindings);
var orderContactMemberAssignment = Expression.Bind(orderPropContact, contactMemberInit);
var orderMemberInit = Expression.MemberInit(Expression.New(typeof(OrderModel).GetConstructors()[0]), new List<MemberAssignment>() {orderContactMemberAssignment});
//during debugging with the same model, I know TModel is OrderModel, so I can cast it
//of course this is just a quick hack to verify it is working correctly in AgGrid, and it is!
return (IQueryable<TModel>)queryOrders.Select(Expression.Lambda<Func<OrderModel, OrderModel>>(orderMemberInit, param));
}
/// <summary>
/// Recursive get the MemberAssignment
/// </summary>
/// <param name="param">The initial paramter expression: var param = Expression.Parameter(typeof(T), "item");</param>
/// <param name="baseType">The type of the model that is being used</param>
/// <param name="propEx">Can be equal to 'param' or when already started with the first property, use: Expression.MakeMemberAccess(param, prop);</param>
/// <param name="properties">The child properties, so not all the properties in the object, but the sub-properties of one property.</param>
/// <param name="index">Index to start at</param>
/// <returns></returns>
public static MemberAssignment RecursiveSelectBindings(ParameterExpression param, Type baseType, Expression propEx, string[] properties, int index)
{
//Get the first property from the list.
var prop = baseType.GetProperty(properties[index], BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase);
var leftProperty = prop;
Expression selectPropEx = Expression.MakeMemberAccess(propEx, prop);
//If this is the last property, then bind it and return that Member assignment
if (properties.Length - 1 == index)
{
var memberAssignment = Expression.Bind(prop, selectPropEx);
return memberAssignment;
}
//If we have more sub-properties, make sure the sub-properties are correctly generated.
//Generate a "new Model() { }"
NewExpression selectSubBody = Expression.New(leftProperty.PropertyType.GetConstructors()[0]);
//Get the binding of the next property (recursive)
var getBinding = RecursiveSelectBindings(param, prop.PropertyType, selectPropEx, properties, index + 1);
MemberInitExpression selectSubMemberInit =
Expression.MemberInit(selectSubBody, new List<MemberAssignment>() { getBinding });
//Finish the binding by generating "new Model() { Property = item.Property.Property }
//During debugging the code, it will become clear what is what.
MemberAssignment selectSubMemberAssignment = Expression.Bind(leftProperty, selectSubMemberInit);
return selectSubMemberAssignment;
}
static Expression Select<T>(this IQueryable<T> source, string[] fields)
{
var itemType = typeof(T);
var groupType = itemType; //itemType.Derive();
var itemParam = Expression.Parameter(itemType, "x");
List<MemberAssignment> bindings = new List<MemberAssignment>();
foreach (var property in fields)
{
Expression propertyExp;
if (property.Contains("."))
{
string[] childProperties = property.Split('.');
var binding = RecursiveSelectBindings(itemParam, itemType, itemParam, childProperties, 0);
bindings.Add(binding);
}
else
{
var fieldValue = groupType.GetProperty(property, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase);
var fieldValueOriginal = Expression.Property(itemParam, fieldValue ?? throw new InvalidOperationException());
var memberAssignment = Expression.Bind(fieldValue, fieldValueOriginal);
bindings.Add(memberAssignment);
}
}
var selector = Expression.MemberInit(Expression.New(groupType), bindings.ToArray());
return Expression.Lambda(selector, itemParam);
}
static IQueryable<IGrouping<T, T>> GroupEntitiesBy<T>(this IQueryable<T> source, string[] fields)
{
var itemType = typeof(T);
var method = typeof(Queryable).GetMethods()
.Where(m => m.Name == "GroupBy")
.Single(m => m.GetParameters().Length == 2)
.MakeGenericMethod(itemType, itemType);
var result = method.Invoke(null, new object[] { source, source.Select(fields) });
return (IQueryable<IGrouping<T, T>>)result;
}
public static IQueryable<TModel> GroupByExpression(List<string> propertyNames, IQueryable<TModel> sequence)
{
var grouping = sequence.GroupBy(propertyNames.ToArray());
var selectParam = Expression.Parameter(grouping.ElementType, "item");
Expression selectPropEx = selectParam;
var selectBody = Expression.New(typeof(TModel).GetConstructors()[0]);
var selectBindings = new List<MemberAssignment>();
foreach (var property in propertyNames)
{
var keyProp = "Key." + property;
//support nested, relation grouping
string[] childProperties = keyProp.Split('.');
var prop = grouping.ElementType.GetProperty(childProperties[0], BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.IgnoreCase);
selectPropEx = Expression.MakeMemberAccess(selectParam, prop);
var binding = PropertyGrouping.RecursiveSelectBindings(selectParam, prop.PropertyType, selectPropEx, childProperties, 1);
selectBindings.Add(binding);
}
MemberInitExpression selectMemberInit = Expression.MemberInit(selectBody, selectBindings);
var queryable = grouping
.Select(Expression.Lambda<Func<IGrouping<TModel, TModel>, TModel>>(selectMemberInit, selectParam));
return queryable;
}
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);
}
}