Построить дерево выражений реляционной группировки С#

Контекст:

Используя 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, потому что я думаю, что это решение, чтобы успешно это сделать.

Уточните, пожалуйста, хотите ли вы группировать по членам одного класса (в примере ContactModel) или хотите создать лямбду с анонимным объектом, например x => new { x.OrderId, x.Contact.ContactId}?

Aleks Andreev 10.04.2019 18:32

Нет, возвращаемый тип не должен быть IQueryable<OrderModel>, поэтому анонимный тип не нужен. То, что я написал для For a single and multiple properties of the same model, должно работать так же, как и для реляционных свойств.

CularBytes 10.04.2019 18:35

А что, если пользователь решит сгруппировать по ClientNumber и OrderNumber? Не могли бы вы показать, как должна выглядеть эта лямбда?

Aleks Andreev 10.04.2019 18:51

Пожалуйста, редактировать свой вопрос и добавьте больше примеров лямбда-выражений, которые вы хотели бы сгенерировать. На данный момент вы показали только один x => x.Contact.ContactId, но ваши генераторы принимают список имен свойств

Aleks Andreev 10.04.2019 18:59

@AleksAndreev Я отредактировал вопрос, вы дали несколько откровений, когда я искал ваши ответы, уже спасибо за это. Я надеюсь, что обновление понятно.

CularBytes 10.04.2019 19:17

Идея select выглядит ошибочной. Даже если вы создадите выражение селектора, запрос вернет то же количество записей, что и источник, а не «группы».

Ivan Stoev 10.04.2019 19:42

@IvanStoev SelectMany, который я представил в своем примере, ошибочен, «рабочая» реализация, которую я представил, действительно работает правильно для свойств той же модели.

CularBytes 10.04.2019 19:45

Все еще не имеет смысла для меня. Для каждого OrderModel элемента в ordersSelect создаст новый OrderModel с некоторыми заполненными свойствами. Как вообще группировка? Если вопрос в том, как создавать динамически Select(item => new OrderModel() { Contact = new ContactModel() { ContactId = item.Contact.ContactId}, OrderId = item.OrderId }), тогда ок, мы, вероятно, можем предоставить решение, но, пожалуйста, обновите вопрос соответствующим образом — удалите все эти объяснения о группировке и вызовите пользовательский метод Select, в настоящее время все это просто сбивает с толку.

Ivan Stoev 10.04.2019 20:00

Лол, в такие моменты ты понимаешь чуть позже, что ты на самом деле записал. Вы правы, выбор ошибочен. Это создавало иллюзию группировки, но повторяется тот же элемент, на котором группируется, в настоящее время группировки нет! 1 шаг вперед, 3 шага назад. Ну, по крайней мере, мы разобрались, как правильно сделать select (чтобы он правильно отображался в списке), теперь часть группировки или отдельный счет?

CularBytes 10.04.2019 21:58
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
9
240
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В этом ответе две части:

  1. Создайте выражение GroupBy и убедитесь, что используется тот же тип возвращаемого значения.
  2. Создайте выражение 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 метода:

https://docs.microsoft.com/en-us/visualstudio/ide/reference/generate-equals-getashcode-methods?view=vs-2019

Убедитесь, что по крайней мере все свойства, которые вы будете использовать в таблице, включены (и могут быть сгруппированы).

Если идея состоит в том, чтобы динамически создать вложенный селектор 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);
    }
}

В основном рекурсивно обрабатывайте пути к элементам, группируйте каждый уровень по имени элемента и привязывайте член либо к исходному выражению, либо к исходному выражению.

Другие вопросы по теме