Контекст:
Используя Ag-Grid, пользователи должны иметь возможность перетаскивать столбцы, которые они хотят сгруппировать.
Скажем, у меня есть следующая модель и группа по функциям:
List<OrderModel> orders = new List<OrderModel>()
{
new OrderModel()
{
OrderId = 184214,
Contact = new ContactModel()
{
ContactId = 1000
}
}
};
var queryOrders = orders.AsQueryable();
Редактировать: Итак, люди дали мне понять, что в приведенном ниже вопросе я на самом деле сосредоточился на динамических Select
правильных элементах (что является одним из требований), я упустил фактическую группировку. Поэтому были внесены некоторые изменения, отражающие обе проблемы: группировка и выбор, строгая типизация.
В соответствии с типом:
Один столбец
IQueryable<OrderModel> resultQueryable = queryOrders
.GroupBy(x => x.ExclPrice)
.Select(x => new OrderModel() { ExclPrice = x.Key.ExclPrice});
Несколько столбцов
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});
Однако последний не работает, определение OrderModel
внутри GroupBy
, по-видимому, вызывает проблемы при переводе его в SQL.
Как мне построить это GroupBy
/ Select
с помощью выражений?
В настоящее время мне удалось выбрать правильные элементы, но группировка еще не выполнена.
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;
}
Это работает нормально, пока я не захочу ввести отношения, поэтому в моем примере 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;
}
Выглядит многообещающе, но, к сожалению, выдает ошибку var memInitExpress = Expression.MemberInit(body, bindings);
ArgumentException ''ContactId' is not a member of type 'OrderModel''
Вот как выглядит выражение при группировке по нескольким столбцам:
Результат Expression.MemberInit(body, bindings)
:
{new OrderModel() {TotalInclPrice = item.TotalInclPrice, OrderId = item.OrderId}}
Итак, все выражение: {item => new OrderModel() {TotalInclPrice = item.TotalInclPrice, OrderId = item.OrderId}}
Итак, теперь не так сложно понять, почему я получаю исключение, о котором я упоминал, просто потому, что оно использует OrderModel
для выбора свойств, а ContactId отсутствует в этой модели. Однако я ограничен и обязан придерживаться IQueryable<OrderModel>
, поэтому теперь вопрос заключается в том, как создать выражение для группировки по ContactId
, используя ту же модель. Я бы предположил, что мне действительно нужно иметь выражение с этим:
Результат Expression.MemberInit(body, bindings)
должен быть:
{new OrderModel() { Contact = new ContactModel() { ContactId = item.Contact.ContactId} , OrderId = item.OrderId}}
. Что-то вроде этого?
Итак, я подумал, давайте вернемся к основам и сделаем это шаг за шагом. В конце концов, цикл for создает следующее выражение. Смотри мой ответ, как я решаю эту часть, Иван ответ, кажется, решил это общим способом, но я еще не тестировал этот код. Однако это еще не группировка, поэтому после применения группировки эти ответы могут больше не работать.
К вашему сведению: AgGrid может найти отношения свойств, просто указав поле столбца contact.contactId
. Поэтому, когда данные загружаются, он просто пытается найти это свойство. Я думаю, что когда вышеприведенное выражение будет создано, оно будет работать в сетке. Я сейчас тоже пробую себя, как создавать суб-MemberInit
, потому что я думаю, что это решение, чтобы успешно это сделать.
Нет, возвращаемый тип не должен быть IQueryable<OrderModel>, поэтому анонимный тип не нужен. То, что я написал для For a single and multiple properties of the same model
, должно работать так же, как и для реляционных свойств.
А что, если пользователь решит сгруппировать по ClientNumber
и OrderNumber
? Не могли бы вы показать, как должна выглядеть эта лямбда?
Пожалуйста, редактировать свой вопрос и добавьте больше примеров лямбда-выражений, которые вы хотели бы сгенерировать. На данный момент вы показали только один x => x.Contact.ContactId
, но ваши генераторы принимают список имен свойств
@AleksAndreev Я отредактировал вопрос, вы дали несколько откровений, когда я искал ваши ответы, уже спасибо за это. Я надеюсь, что обновление понятно.
Идея select
выглядит ошибочной. Даже если вы создадите выражение селектора, запрос вернет то же количество записей, что и источник, а не «группы».
@IvanStoev SelectMany
, который я представил в своем примере, ошибочен, «рабочая» реализация, которую я представил, действительно работает правильно для свойств той же модели.
Все еще не имеет смысла для меня. Для каждого OrderModel
элемента в orders
Select
создаст новый OrderModel
с некоторыми заполненными свойствами. Как вообще группировка? Если вопрос в том, как создавать динамически Select(item => new OrderModel() { Contact = new ContactModel() { ContactId = item.Contact.ContactId}, OrderId = item.OrderId })
, тогда ок, мы, вероятно, можем предоставить решение, но, пожалуйста, обновите вопрос соответствующим образом — удалите все эти объяснения о группировке и вызовите пользовательский метод Select
, в настоящее время все это просто сбивает с толку.
Лол, в такие моменты ты понимаешь чуть позже, что ты на самом деле записал. Вы правы, выбор ошибочен. Это создавало иллюзию группировки, но повторяется тот же элемент, на котором группируется, в настоящее время группировки нет! 1 шаг вперед, 3 шага назад. Ну, по крайней мере, мы разобрались, как правильно сделать select
(чтобы он правильно отображался в списке), теперь часть группировки или отдельный счет?
В этом ответе две части:
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;
}
ВЫБРАТЬ
Итак, мы сделали выражение 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;
}
ПолучитьHashCode()
К сожалению, это все еще не работало, пока я не начал внедрять GetHasCode()
и Equals()
в каждую используемую модель. Во время Count()
или выполнения запроса путем выполнения .ToList()
он сравнивает все объекты, чтобы убедиться, что объекты равны (или нет) друг другу. Если они равны: Одна и та же группа. Но поскольку мы генерировали эти модели на лету, у него нет возможности правильно сравнивать эти объекты на основе расположения в памяти (по умолчанию).
К счастью, вы можете очень легко сгенерировать эти 2 метода:
Убедитесь, что по крайней мере все свойства, которые вы будете использовать в таблице, включены (и могут быть сгруппированы).
Если идея состоит в том, чтобы динамически создать вложенный селектор MemberInit
, это можно сделать следующим образом:
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);
}
}
В основном рекурсивно обрабатывайте пути к элементам, группируйте каждый уровень по имени элемента и привязывайте член либо к исходному выражению, либо к исходному выражению.
Уточните, пожалуйста, хотите ли вы группировать по членам одного класса (в примере
ContactModel
) или хотите создать лямбду с анонимным объектом, напримерx => new { x.OrderId, x.Contact.ContactId}
?