C# LINQ,按 [Key] 属性动态分组
C# LINQ, dynamic grouping by [Key] attributes
考虑以下 classes:
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; }
}
每个 class 都有带有 [Key] 属性的属性。是否可以通过它们各自的 [Key] 属性对这些 classes 中的任何一个的 IEnumerable 进行动态分组?
我会为每个类型添加扩展方法,例如
选项 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 => p.GetPrimaryKey())
.
选项 3:
反射解决方案是可能的,但会很慢。素描(远production-ready!)
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 => CombinedKey.GetKey(p))
这里的挑战是您需要构建一个匿名类型,以便拥有一个可以转换为 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);
}
下面是你将如何使用它(注意:传递给方法的虚拟匿名类型是为了使匿名类型成为 compile-time 类型,该方法不关心值是什么你通过了它。):
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 }
有人发布了一个有效答案,后来出于某种原因将其删除。这是:
组合键class:
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 的解决方案是可行的。
考虑以下 classes:
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; }
}
每个 class 都有带有 [Key] 属性的属性。是否可以通过它们各自的 [Key] 属性对这些 classes 中的任何一个的 IEnumerable 进行动态分组?
我会为每个类型添加扩展方法,例如
选项 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 => p.GetPrimaryKey())
.
选项 3:
反射解决方案是可能的,但会很慢。素描(远production-ready!)
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 => CombinedKey.GetKey(p))
这里的挑战是您需要构建一个匿名类型,以便拥有一个可以转换为 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);
}
下面是你将如何使用它(注意:传递给方法的虚拟匿名类型是为了使匿名类型成为 compile-time 类型,该方法不关心值是什么你通过了它。):
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 }
有人发布了一个有效答案,后来出于某种原因将其删除。这是:
组合键class:
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 的解决方案是可行的。