Search code examples
c#linqgroupingcustom-attributeslinq-method-syntax

C# LINQ, dynamic grouping by [Key] attributes


Consider the following 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; }
}

Each class has properties with [Key] attribute. Is it possible to dynamically group an IEnumerable of any of these classes by their respective [Key] attributes?


Solution

  • Somebody had posted a valid answer and removed it later for some reason. Here it is:

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

    Caller function and the actual usage:

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

    Yes, it is relatively slow, so if it is a problem, then Klaus Gütter's solutions are the way to go.