【问题标题】:C# LINQ, dynamic grouping by [Key] attributesC# LINQ,通过 [Key] 属性进行动态分组
【发布时间】:2020-08-30 13:59:51
【问题描述】:

考虑以下类:

public class Potato
{
    [Key]
    public string Farm { get; set; }
    [Key]
    public int Size { get; set; }
    public string Trademark { get; set; }
}

public class Haybell
{
    [Key]
    public string color { get; set; }
    public int StrawCount { get; set; }
}

public class Frog
{
    [Key]
    public bool IsAlive { get; set; }
    [Key]
    public bool IsVirulent { get; set; }
    public byte LimbCount { get; set; } = 4;
    public ConsoleColor Color { get; set; }
}

每个类都有带有 [Key] 属性的属性。是否可以通过它们各自的 [Key] 属性对这些类中的任何一个 IEnumerable 进行动态分组?

【问题讨论】:

  • “动态”是什么意思?使用反射构造 GroupBy 表达式?你为什么要这样做?
  • 是的,这就是我要找的。因为那样我就可以做类似 .GroupByKey() 之类的事情,而不必为我拥有的每个类指定 Keys
  • 你真的需要动态 GroupBy 表达式(用于 IQueryable)吗?如果是这样,伊恩默瑟回答了这一点。 Klaus Gütters 的答案要么不是动态的,要么不适用于 IQueryable。我很困惑你需要什么。

标签: c# linq grouping custom-attributes linq-method-syntax


【解决方案1】:

我会为您的每种类型添加扩展方法,例如

选项 1:

static class Extensions 
{
    public static IEnumerable<IGrouping<Tuple<string, int>, Potato>>
       GroupByPrimaryKey(this IEnumerable<Potato> e)
    {
        return e.GroupBy(p => Tuple.Create(p.Farm, p.Size));
    }

    public static IEnumerable<IGrouping<Tuple<bool, bool>, Frog>>
       GroupByPrimaryKey(this IEnumerable<Frog> e)
    {
        return e.GroupBy(p => Tuple.Create(p.IsAlive, p.IsVirulent));
    }
}

如果类型很多,可以使用t4生成代码。

用法:.GroupByPrimaryKey()

选项 2:

一个更简单的变化:

static class Extensions 
{
    public static Tuple<string, int> GetPrimaryKey(this Potato p)
    {
        return Tuple.Create(p.Farm, p.Size);
    }
    public static Tuple<bool, bool> GetPrimaryKey(this Frog p)
    {
        return Tuple.Create(p.IsAlive, p.IsVirulent);
    }

}

用法:.GroupBy(p =&gt; p.GetPrimaryKey())

选项 3:

反射的解决方案是可能的,但会很慢。草图(远未准备好生产!)

class CombinedKey : IEquatable<CombinedKey>
{
    object[] _keys;
    CombinedKey(object[] keys)
    {
        _keys = keys;
    }
    
    public bool Equals(CombinedKey other)
    {
        return _keys.SequenceEqual(other._keys);
    }
    
    public override bool Equals(object obj)
    {
        return obj is CombinedKey && Equals((CombinedKey)obj);
    }
    
    public override int GetHashCode()
    {
        return 0;
    }

    public static CombinedKey GetKey<T>(T instance)
    {
        return new CombinedKey(GetKeyAttributes(typeof(T)).Select(p => p.GetValue(instance, null)).ToArray());
    }

    private static PropertyInfo[] GetKeyAttributes(Type type)
    {
        // you definitely want to cache this
        return type.GetProperties()
            .Where(p => Attribute.GetCustomAttribute(p, typeof(KeyAttribute)) != null)
            .ToArray();
    }
}   

用法:GroupBy(p =&gt; CombinedKey.GetKey(p))

【讨论】:

  • 我想你会发现LINQ to SQL不支持Tuple.Create,你需要使用匿名类型。
  • @ianmercer OP 没有指定它是 Linq-To-Sql,但如果是,你可能是对的。
【解决方案2】:

这里的挑战是您需要构建一个匿名类型,以便拥有一个可以转换为 SQL 或任何其他 LINQ 提供程序的 GroupBy 表达式。

我不确定您是否可以使用反射来做到这一点(并非没有一些非常复杂的代码来在运行时创建匿名类型)。但是,如果您愿意提供匿名类型的示例作为种子,您可以创建分组表达式。

public static Expression<Func<TSource, TAnon>> GetAnonymous<TSource,TAnon>(TSource dummy, TAnon example)
{
  var ctor = typeof(TAnon).GetConstructors().First();
  var paramExpr = Expression.Parameter(typeof(TSource));
  return Expression.Lambda<Func<TSource, TAnon>>
  (
      Expression.New
      (
          ctor,
          ctor.GetParameters().Select
          (
              (x, i) => Expression.Convert
              (
                  Expression.Property(paramExpr, x.Name),   // fetch same named property
                  x.ParameterType
              )
          )
      ), paramExpr);
}

这是你将如何使用它(注意:传递给方法的虚拟匿名类型是为了使匿名类型成为编译时类型,该方法不关心你传入的值是什么为它。):

static void Main()
{
    
    var groupByExpression = GetAnonymous(new Frog(), new {IsAlive = true, IsVirulent = true});
    
    Console.WriteLine(groupByExpression);
    
    var frogs = new []{ new Frog{ IsAlive = true, IsVirulent = false}, new Frog{ IsAlive = false, IsVirulent = true}, new Frog{ IsAlive = true, IsVirulent = true}};
    
    var grouped = frogs.AsQueryable().GroupBy(groupByExpression);
    
    foreach (var group in grouped)
    {
       Console.WriteLine(group.Key);    
    }
    
}   

产生:

Param_0 => new <>f__AnonymousType0`2(Convert(Param_0.IsAlive, Boolean), Convert(Param_0.IsVirulent, Boolean))
{ IsAlive = True, IsVirulent = False }
{ IsAlive = False, IsVirulent = True }
{ IsAlive = True, IsVirulent = True }

【讨论】:

  • 我完全没有得到这个答案。到目前为止,来自 Klaus Gütter 的选项 #1 似乎确实是可行的方法,即使它不是很动态
【解决方案3】:

有人发布了一个有效的答案,后来由于某种原因将其删除。这里是:

组合键类:

class CombinedKey<T> : IEquatable<CombinedKey<T>>
{
    readonly object[] _keys;

    public bool Equals(CombinedKey<T> other)
    {
        return _keys.SequenceEqual(other._keys);
    }

    public override bool Equals(object obj)
    {
        return obj is CombinedKey<T> key && Equals(key);
    }

    public override int GetHashCode()
    {
        int hash = _keys.Length;
        foreach (object o in _keys)
        {
            if (o != null)
            {
                hash = hash * 13 + o.GetHashCode();
            }
        }
        return hash;
    }

    readonly Lazy<Func<T, object[]>> lambdaFunc = new Lazy<Func<T, object[]>>(() =>
    {
        Type type = typeof(T);
        var paramExpr = Expression.Parameter(type);
        var arrayExpr = Expression.NewArrayInit(
            typeof(object),
            type.GetProperties()
                .Where(p => (Attribute.GetCustomAttribute(p, typeof(KeyAttribute)) != null))
                .Select(p => Expression.Convert(Expression.Property(paramExpr, p), typeof(object)))
                .ToArray()
            );

        return Expression.Lambda<Func<T, object[]>>(arrayExpr, paramExpr).Compile();
    }, System.Threading.LazyThreadSafetyMode.PublicationOnly);

    public CombinedKey(T instance)
    {
        _keys = lambdaFunc.Value(instance);
    }
}

调用函数及实际用法:

public static class MyClassWithLogic
{
    //Caller to CombinedKey class
    private static CombinedKey<Q> NewCombinedKey<Q>(Q instance)
    {
        return new CombinedKey<Q>(instance);
    }

    //Extension method for IEnumerables
    public static IEnumerable<T> DistinctByPrimaryKey<T>(this IEnumerable<T> entries) where T : class
    {
        return entries.AsQueryable().GroupBy(NewCombinedKey)
            .Select(r => r.First());
    }
}

是的,它比较慢,所以如果是一个问题,那么 Klaus Gütter 的解决方案就是要走的路。

【讨论】:

    猜你喜欢
    • 2023-04-10
    • 1970-01-01
    • 2015-12-19
    • 1970-01-01
    • 2013-01-23
    • 1970-01-01
    • 2020-07-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多