【问题标题】:How to group by multiple generic linq expressions如何按多个通用 linq 表达式分组
【发布时间】:2012-04-13 18:26:56
【问题描述】:

我正在尝试使用 Linq 表达式来构建查询,但我一直在尝试按多列进行分组。假设我有一个基本收藏:

IEnumerable<Row> collection = new Row[]
{
    new Row() { Col1 = "a", Col2="x" },
    new Row() { Col1 = "a", Col2="x" },
    new Row() { Col1 = "a", Col2="y" },
};

我知道您可以使用 lambda 表达式对它们进行分组:

foreach (var grp in collection.GroupBy(item => new { item.Col1, item.Col2 }))
{
    Debug.Write("Grouping by " + grp.Key.Col1 + " and " + grp.Key.Col2 + ": ");
    Debug.WriteLine(grp.Count() + " rows");
}

如您所见,此分组正确:

Grouping by a and x: 2 rows
Grouping by a and y: 1 rows

但是现在,假设我收到一组选择器来分组,它作为我方法中的参数传递给我,并且实体类型是通用的:

void doLinq<T>(params Expression<Func<T,object>>[] selectors)
{
    // linq stuff
}

调用该方法的人会这样调用:

doLinq<Row>(entity=>entity.Col1, entity=>entity.Col2);

如何构造 group-by 表达式?

foreach (var grp in collection.GroupBy(
      item => new { 
          // selectors??
      }))
{
    // grp.Key. ??
}

编辑

我在上面进行了更新,希望能阐明为什么我需要这组选择器。

编辑#2

使 doLinq 中的实体类型通用。

【问题讨论】:

  • 似乎您应该能够编写一个函数,将几个Expression&lt;Func&lt;Row,object&gt;&gt; 组合成一个Expression&lt;Func&lt;Row,object&gt;&gt;,返回一个元组或一个值数组或其他任何东西。看起来您正在使用 linq to sql,但是我对 sql 的翻译了解不足,无法知道您是否可以以可翻译的方式执行此操作。
  • @phoog 看起来你应该能够将它们组合成一个表达式——我仍然不知道该怎么做。我最终为每一行组合了表达式的结果。
  • 您应该将您的解决方案作为新答案发布,而不是编辑问题。

标签: c# .net linq lambda linq-expressions


【解决方案1】:

【讨论】:

  • 非常有趣和有用的链接,但这不是我想要的。 Mitsu 在层次关系中进行子分组,但每个分组都是在一个键上完成的——即按国家分组,然后按城市分组。我希望有一个单一级别的分组,但键中有多个属性,例如,按国家和年龄段分组(在我的示例中为 Col1 和 Col2)。
  • 对不起,我错过了目标 :) 这是一个与我的意思类似的问题作为解决方案的链接:stackoverflow.com/questions/3929041/… By Dynamic Linq 我建议使用基于字符串的 linq 接口。
【解决方案2】:

好吧,我假设您使用 linq-to-sql 或类似的东西,所以您需要表达式树。如果没有,可能还有其他可能性。

我可以看到的可能解决方案:

  • 动态 linq

请参阅 Vladimir Perevalovs 的回答。

  • 手动构建整个 groupby 表达式树

http://msdn.microsoft.com/en-us/library/bb882637.aspx

  • 丑陋的解决方法

嗯,那是我的部门:)

未经测试的代码:

 void doLinq(params string[] selectors) // checking two expressions for equality is messy, so I used strings
     foreach (var grp in collection.GroupBy(
          item => new { 
              Col1 = (selectors.Contains("Col1") ? item.Col1 : String.Empty),
              Col2 = (selectors.Contains("Col2") ? item.Col2 : String.Empty)
              // need to add a line for each column :(
          }))
     {
          string[] grouping = (new string[]{grp.Key.Col1, grp.Key.Col2 /*, ...*/ }).Where(s=>!s.IsNullOrEmpty()).ToArray();
          Debug.Write("Grouping by " + String.Join(" and ", grouping)+ ": ");
          Debug.WriteLine(grp.Count() + " rows");
     }
 }

【讨论】:

  • 我意识到一开始我把这个问题简化得太多了——它掩盖了我想要做的事情。 “Row”类型实际上是通用的(参见上面的更新),这似乎排除了#1 和#3。 #2 看起来很有趣,我正在查看...
【解决方案3】:

我对 linq-to-sql 的了解非常有限,但是 GroupBy 中的内容真的很重要吗?因为如果不是,您可以推出自己的 keySelector。无论如何,我用 Sql Server CE 和 Sql Server Express 都试过了,这似乎可行:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq.Expressions;

namespace ConsoleApplication1 {
    class Props {
        public List<object> list = new List<object>();
        public override bool Equals(object obj) {
            return Enumerable.SequenceEqual(list, (obj as Props).list);
        }
        public override int GetHashCode() {
            return list.Select(o => o.GetHashCode()).Aggregate((i1, i2) => i1 ^ i2);
        }
    }
    class Program {
        static void Main(string[] args) {
            Lol db = new Lol(@"Data Source=.\SQLExpress;Initial Catalog=Lol;Integrated Security=true");
            db.Log = Console.Out;
            doLinq(db.Test, row => row.Col1, row => row.Col2);
            Console.ReadLine();
        }
        static void doLinq<T>(Table<T> table, params Func<T, object>[] selectors) where T : class {
            Func<T, Props> selector = item => {
                var props = new Props();
                foreach (var sel in selectors) props.list.Add(sel(item));
                return props;
            };
            foreach (var grp in table.GroupBy(selector)) {
                Console.Write("Grouping by " + string.Join(", ", grp.Key.list) + ": ");
                Console.WriteLine(grp.Count() + " rows");
            }
        }
    }
}

Lol 数据库有一个三行表“Test”。输出是这样的:

SELECT [t0].[Col1], [t0].[Col2]
FROM [dbo].[Test] AS [t0]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1

Grouping by a, x: 2 rows
Grouping by a, y: 1 rows

我检查了查询,似乎 linq-to-sql 足够聪明,不会在不能为 groupBy 生成 sql 时,所以它会遍历表的所有行,然后在客户端对它们进行分组。

编辑:为了完成而添加了一些小细节,连接字符串现在假定为 Sql Server Express。

【讨论】:

  • 巧妙,但我无法让它工作......我不知道为什么。照原样,“Props.Equals”方法在我的测试用例中总是返回 true;它似乎是在比较 Linq 表达式而不是评估。你不需要在那里的某个地方使用“Expression.Compile()”吗?
  • 好吧,GroupByProps 组织成一个哈希表,所以Props.Equals 只会被Props.GetHashCode 返回相等值的对象调用,这意味着它们很可能已经相等。所以就其本身而言,Props.Equals 总是返回 true 几乎不是问题。您也不需要在代码中的任何地方使用Expressions,因为整个分组发生在客户端上,并且生成的sql 是来自表的简单select。我还用 SQL Server Express 进行了尝试,结果相同。
【解决方案4】:

该解决方案对我有用。它涉及两个部分:

  • 在给定行值和选择器集的情况下创建一个分组对象(我将其实现为 object[] 并不优雅)。这涉及编译和调用行项目上的每个选择器的 lambda 表达式。
  • 为分组对象类型实现 IEquality(在我的例子中是 IEqualityComparer)。

第一部分

foreach (System.Linq.IGrouping<object[], T> g in collection.GroupBy(
    new Func<T, object[]>(
        item => selectors.Select(sel => sel.Compile().Invoke(item)).ToArray()
    ),
    new ColumnComparer()
)
{ ... }

第二部分

public class ColumnComparer : IEqualityComparer<object[]>
{
    public bool Equals(object[] x, object[] y)
    {
        return Enumerable.SequenceEqual(x, y);
    }

    public int GetHashCode(object[] obj)
    {
        return (string.Join("", obj.ToArray())).GetHashCode();
    }
}

这适用于基本的 Linq,而 Linq 则适用于 MySql 连接器。其他哪些 Linq 提供程序,以及它适用于哪种表达式类型是另一个问题......

【讨论】:

  • 我认为这将查询数据库的所有行,然后在本地执行分组。因此,这不适用于带有将查询转换为 t-sql 的 ms-sql 服务器的 linq-to-sql,您需要一个表达式树。尽管如此,即使在 linq-to-sql 中,您也可以始终在 GroupBy() 之前添加 .AsEnumerable() 以强制在本地执行分组
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多