使用动态列进行LINQ分组

10

我有这样一个表格:

 variety category  quantity
----------------------------------------------
  rg      pm         10
  gs      pm          5
  rg      com         8

我希望根据以下两个bool类型参数进行分组:IncludeVarietyIncludeCategory

例如:

IncludeVariety = true;
IncludeCategory = true;

会返回这个:

 variety category  quantity
----------------------------------------------
  rg      pm         10
  gs      pm          5
  rg      com         8

还有这个:

IncludeVariety = true;
IncludeCategory = false;

会返回这个:

  variety category  quantity
----------------------------------------------
    rg      -         18
    gs      -          5

以及这个:

IncludeVariety = false;
IncludeCategory = true;

会返回这个:

  variety category  quantity
----------------------------------------------
     -      pm         15
     -      com         8

你明白我的意思...

问题: 我怎样能用LINQ实现这个?

重要提示:我已将问题简化为 两个布尔变量 (IncludeVarietyIncludeCategory) ,但实际上我将拥有更多的列 (比如说五个)

我想不出如何动态生成查询(使用.GroupBy.Select):

 rows.GroupBy(r => new { r.Variety, r.Category })
 .Select(g => new 
  {
        Variety = g.Key.Variety,
        Category = g.Key.Category,
        Quantity = g.Sum(a => a.Quantity),
  });

 rows.GroupBy(r => new { r.Category })
 .Select(g => new 
  {
        Variety = new {},
        Category = g.Key.Category,
        Quantity = g.Sum(a => a.Quantity),
  });

 rows.GroupBy(r => new { r.Variety })
 .Select(g => new 
  {
        Variety = g.Key.Variety,
        Category = new {},
        Quantity = g.Sum(a => a.Quantity),
  });

我过去做过类似的事情,就是连接Where,比如:

  var query = ...

  if (foo) {
       query = query.Where(...)
  }

  if (bar) {
       query = query.Where(...)
  }

  var result = query.Select(...)

我是否可以在这里做类似的事情?

3个回答

18
var results=items
  .Select(i=>
    new {
      variety=includevariety?t.variety:null,
      category=includecategory?t.category:null,
      ...
    })
  .GroupBy(g=>
    new { variety, category, ... }, g=>g.quantity)
  .Select(i=>new {
    variety=i.Key.variety,
    category=i.Key.category,
    ...
    quantity=i.Sum()
  });
var results=items
  .GroupBy(g=>
    new {
      variety=includevariety?t.variety:null,
      category=includecategory?t.category:null,
      ... 
    }, g=>g.quantity)
  .Select(i=>new {
    variety=i.Key.variety,
    category=i.Key.category,
    ...
    quantity=i.Sum()
  });

你真是个天才!我会尝试这个方法,如果有效的话,我会接受这个答案。 - sports
如果你愿意的话,你也可以通过将第一个select和groupby组合起来来缩短它,但我认为这样更易读,但这非常主观。 - Robert McKee
哦,好的。我忘记了空条件。我删除我的答案 xD - M.kazem Akhgary
老铁,它完美地运行了!我还有一些ID,如果有人感兴趣:Category = includeCategory ? r.Category : null, CategoryId = includeCategory ? r.CategoryId : (long?)null -- 需要那个(long?)null - sports

8

如果您需要这个功能真正地动态化,可以使用Scott Gu的Dynamic LINQ库。

您只需要确定要包含在结果中的列,并按它们分组即可。

public static IQueryable GroupByColumns(this IQueryable source,
    bool includeVariety = false,
    bool includeCategory = false)
{
    var columns = new List<string>();
    if (includeVariety) columns.Add("Variety");
    if (includeCategory) columns.Add("Category");
    return source.GroupBy($"new({String.Join(",", columns)})", "it");
}

那你可以将它们分组。
var query = rows.GroupByColumns(includeVariety: true, includeCategory: true);

如果您需要动态结果,这是一个非常好的解决方案。我通常尽可能避免使用它,但有时您只能使用它,而这个答案就是完美的选择。 - Robert McKee
为什么它比你的答案更好? - sports

0
无论如何,如果有人仍然需要按动态列进行分组而不使用Dynamic LINQ并具有类型安全性。您可以向此IQueryable扩展方法提供一个匿名对象,其中包含您可能想要按其分组的所有属性(具有匹配的类型!)以及您要用于此组调用的属性名称列表。在第一个重载中,我使用匿名对象获取它的构造函数和属性。我使用这些动态构建分组表达式。在第二个重载中,我仅使用匿名对象来推断TKey的类型,不幸的是,在C#中没有办法绕过它,因为它具有有限的类型别名能力。
仅适用于可空属性,像这样,可能很容易扩展为非空值,但现在无法打扰
public static IQueryable<IGrouping<TKey, TElement>> GroupByProps<TElement, TKey>(this IQueryable<TElement> self, TKey model, params string[] propNames)
{
    var modelType = model.GetType();
    var props = modelType.GetProperties();
    var modelCtor = modelType.GetConstructor(props.Select(t => t.PropertyType).ToArray());

    return self.GroupByProps(model, modelCtor, props, propNames);
}

public static IQueryable<IGrouping<TKey, TElement>> GroupByProps<TElement, TKey>(this IQueryable<TElement> self, TKey model, ConstructorInfo modelCtor, PropertyInfo[] props, params string[] propNames)
{
    var parameter = Expression.Parameter(typeof(TElement), "r");
    var propExpressions = props
        .Select(p =>
        {
            Expression value;

            if (propNames.Contains(p.Name))
                value = Expression.PropertyOrField(parameter, p.Name);
            else
                value = Expression.Convert(Expression.Constant(null, typeof(object)), p.PropertyType);

            return value;
        })
        .ToArray();

    var n = Expression.New(
        modelCtor,
        propExpressions,
        props
    );

    var expr = Expression.Lambda<Func<TElement, TKey>>(n, parameter);
    return self.GroupBy(expr);
}

我实现了两个重载,以防每次调用时都要使用反射缓存构造函数和属性。使用方法如下:

//Class with properties that you want to group by
class Record
{
    public string Test { get; set; }
    public int? Hallo { get; set; }
    public DateTime? Prop { get; set; }

    public string PropertyWhichYouNeverWantToGroupBy { get; set; }
}

//usage

IQueryable<Record> queryable = ...; //the queryable

var grouped = queryable.GroupByProps(new
{
    Test = (string)null,        //put all properties that you might want to group by here
    Hallo = (int?)null,
    Prop = (DateTime?)null
}, nameof(Record.Test), nameof(Record.Prop));   //This will group by Test and Prop but not by Hallo

//Or to cache constructor and props
var anonymous = new
{
    Test = (string)null,        //put all properties that you might want to group by here
    Hallo = (int?)null,
    Prop = (DateTime?)null
};
var type = anonymous.GetType();
var constructor = type.GetConstructor(new[]
{
    typeof(string),             //Put all property types of your anonymous object here 
    typeof(int?),
    typeof(DateTime?)
});
var props = type.GetProperties();
//You need to keep constructor and props and maybe anonymous 
//Then call without reflection overhead
queryable.GroupByProps(anonymous, constructor, props, nameof(Record.Test), nameof(Record.Prop));

作为TKey接收到的匿名对象只会填充你用于分组的键(在这个例子中是"Test"和"Prop"),其他键将为空。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接