简体   繁体   English

构建关系分组表达式树c#

[英]Build relational grouping expression tree c#

Context: 语境:

Using Ag-Grid, users should be able to drag-drop columns they want to group on. 使用Ag-Grid,用户应该能够拖放他们想要分组的列。

在此输入图像描述

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. 编辑:所以人们让我意识到,在下面的问题中,我实际上是专注于动态Select正确的项目(这是要求之一),我错过了实际进行分组。 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. 但是,最后一个不起作用,在GroupBy定义OrderModel显然在将其转换为SQL时会出现问题。

How do I build this GroupBy / Select using Expressions? 如何使用表达式构建此GroupBy / Select

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 . 这工作正常,直到我想介绍一个关系,所以在我的例子中, 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); 可能看起来很有希望,但不幸的是,它在var memInitExpress = Expression.MemberInit(body, bindings);抛出一个错误var memInitExpress = Expression.MemberInit(body, bindings);

ArgumentException ''ContactId' is not a member of type 'OrderModel'' ArgumentException''ContactId'不是'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}} Expression.MemberInit(body, bindings)是: {new OrderModel() {TotalInclPrice = item.TotalInclPrice, OrderId = item.OrderId}}

So the entire expression is: {item => new OrderModel() {TotalInclPrice = item.TotalInclPrice, OrderId = item.OrderId}} 所以整个表达式是: {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. 所以现在要理解为什么我得到我提到的异常并不是那么困难,因为它使用OrderModel来选择属性,而ContactId不在该模型中。 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. 但是我受限并且需要坚持IQueryable<OrderModel> ,因此现在的问题是如何使用相同的模型创建表达式以通过ContactId进行分组。 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}} . Expression.MemberInit(body, bindings)结果需要是: {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. 最终,for循环创建以下表达式。 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. 看到我的答案我是如何解决这一部分的, Ivan的答案似乎已经以一般方式解决了这个问题,但我还没有测试过这个代码。 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 . 仅供参考:AgGrid只需提供列字段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. 我认为当创建上面的表达式时,它将在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. 我现在正在尝试如何创建子MemberInit ,因为我认为这是成功完成它的解决方案。

If the idea is to create dynamically a nested MemberInit selector, it could be done as follows: 如果想要动态创建嵌套的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);
    }
}

Basically process member paths recursively, group each level by member name and bind the member to either source expression or MemberInit of source expression. 基本上是递归处理成员路径,按成员名称对每个级别进行分组,并将成员绑定到源表达式或源表达式的MemberInit

There are two parts in this answer: 这个答案有两个部分:

  1. Create a GroupBy expression and make sure the same return type is used. 创建GroupBy表达式并确保使用相同的返回类型。
  2. Create a Select expression from the result of the GroupBy expression GroupBy表达式的结果创建Select表达式

SELECT & GROUPING - non-generic SELECT&GROUPING - 非泛型

So, the complete solution is below, but to give you an idea on how this works, see this piece of code, this is written in a non-generic version. 因此,完整的解决方案如下,但为了让您了解其工作原理,请参阅此代码段,这是以非通用版本编写的。 The code for grouping is almost the same, the tiny difference is that a Key. 分组的代码几乎相同,微小的区别就是Key. property is added to the beginning. 属性添加到开头。

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));
}

So now we need to do that in a generic way. 所以现在我们需要以通用的方式做到这一点。

Grouping: 分组:

To do the grouping in a generic way, I found this amazing post , he deserves a ton of credit to develop that part. 为了以通用的方式进行分组,我找到了这个惊人的帖子 ,他应该得到大量的信任来开发这一部分。 However I had to modify it to make sure it also supports sub-relationships. 但是我必须修改它以确保它也支持子关系。 In my example: Order.Contact.ContactId . 在我的例子中: Order.Contact.ContactId

I first wrote this recursive method to correctly get the MemberAssignment bindings. 我首先编写了这个递归方法来正确获取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;
    }

Then thereafter, I could modify the Select<T> method of the post I mentioned : 然后,我可以修改我提到帖子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);
    }

This above code is called by below code (which I didn't modify), but you can see it returns IQueryable<IGrouping<T,T>> type. 上面的代码由下面的代码调用(我没有修改),但你可以看到它返回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 选择

So we have now done the GroupBy expression, what we now need to do is the Select expression. 所以我们现在已经完成了GroupBy表达式,我们现在需要做的是Select表达式。 As I said before it is almost equal to the GroupBy, the only difference is that we have to add Key. 正如我之前所说,它几乎与GroupBy相同,唯一的区别是我们必须添加Key. infront of each property. 每个属性的面前。 This is because the Key is the result of the GroupBy , hence you need to start with this. 这是因为KeyGroupBy的结果,因此您需要从此开始。

    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() GetHashCode的()

Unfortunately, this still didn't work, up until I started implementing GetHasCode() and Equals() in each model that is used. 不幸的是,直到我开始在每个使用的模型中实现GetHasCode()Equals()之前,这仍然无效。 During Count() or executing the query by doing .ToList() it will compare all objects to make sure objects are equal (or not) to each other. Count()期间或通过执行.ToList()执行查询时,它将比较所有对象以确保对象彼此相等(或不相等)。 If they are equal: Same group. 如果他们是平等的:相同的组。 But because we generated those models on the fly, it does not have a way to correctly compare those objects based on memory location (by default). 但是因为我们在运行中生成了这些模型,所以它没有办法根据内存位置正确比较这些对象(默认情况下)。

Luckily, you can generate those 2 methods very easily: 幸运的是,您可以非常轻松地生成这两种方法:

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

Make sure atleast all properties are included that you will use in the table (and can be grouped by). 确保至少包含您将在表中使用的所有属性(并且可以按分组)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM