Этот ответ состоит из двух частей:
- Создайте выражение
GroupBy
и убедитесь, что используется тот же тип возврата. - Создайте выражение
Select
изрезультат выражения GroupBy
SELECT & GROUPING - неуниверсальный
Итак, полное решение приведено ниже, но чтобы дать вам представление окак это работает, смотрите этот фрагмент кода, это написано в неуниверсальной версии.Код для группировки практически одинаков, крошечная разница в том, что к началу добавлено свойство Key.
.
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));
}
Так что теперь нам нужно сделать это общим способом.
Группировка:
Чтобы сделать группировку в общем виде, я нашел этот удивительный пост , он заслуживает кучу кредитов для разработки этой части.Однако я должен был изменить это, чтобы удостовериться, что это также поддерживает подчиненные отношения.В моем примере: Order.Contact.ContactId
.
Сначала я написал этот рекурсивный метод для правильного получения привязок MemberAssignment
.
/// <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;
}
Затем я мог бы изменить метод Select<T>
пост , о котором я упоминал :
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);
}
Этот код выше вызывается кодом ниже (который я не модифицировал), но вы можете видеть, что он возвращает IQueryable<IGrouping<T,T>>
тип.
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;
}
SELECT
Итак, мы выполнили выражение GroupBy
, теперь нам нужно сделать выражение Select
.Как я уже говорил, он почти равен GroupBy, единственное отличие состоит в том, что мы должны добавить Key.
перед каждым свойством.Это потому, что Key
является результатом GroupBy
, поэтому вам нужно начать с этого.
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;
}
GetHashCode ()
К сожалению,это все еще не работало, пока я не начал внедрять GetHasCode()
и Equals()
в каждой используемой модели.Во время Count()
или выполнения запроса с помощью .ToList()
он будет сравнивать все объекты, чтобы убедиться, что объекты равны (или нет) друг другу.Если они равны: та же группа.Но поскольку мы сгенерировали эти модели на лету, у них нет способа правильно сравнить эти объекты в зависимости от места в памяти (по умолчанию).
К счастью, вы можете очень легко сгенерировать эти 2 метода:
https://docs.microsoft.com/en-us/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2019
Убедитесь, что включены по крайней мере все свойства, которые вы будете использовать в таблице (и могут быть сгруппированы).