1

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?

Nick Farsi
  • 164
  • 1
  • 8
  • 1
    What do you mean by "dynamically"? To construct the GroupBy expression using reflection? Why would you want to do this? – Klaus Gütter Aug 30 '20 at 14:12
  • Yes, this is what I'm looking for. Because then I would be able to do something like .GroupByKey() without having to specify Keys for every class that I have – Nick Farsi Aug 30 '20 at 14:45
  • Do you really need dynamic GroupBy expressions (for IQueryable)? If so, Ian Mercer anwered that. Klaus Gütters answers are either not dynamic, or not for IQueryable. I am confused what you need. – Antonín Lejsek Aug 30 '20 at 18:28

3 Answers3

2

I would go for adding extension methods for each your types, like

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

If there are lots of types, you may generate the code using t4.

Usage: .GroupByPrimaryKey().

Option 2:

A simpler variation:

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

}

Usage: .GroupBy(p => p.GetPrimaryKey()).

Option 3:

A solution with reflection is possible, but will be slow. Sketch (far from 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();
    }
}   

Usage: GroupBy(p => CombinedKey.GetKey(p))

Klaus Gütter
  • 6,283
  • 4
  • 24
  • 30
1

The challenge here is that you need to build an anonymous type in order to have a GroupBy Expression that can translate to SQL or any other LINQ provider.

I'm not sure that you can do that using reflection (not without some really complex code to create an anonymous type at runtime). But you could create the grouping expression if you were willing to provide an example of the anonymous type as the seed.

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

And here's how you would use it (Note: the dummy anonymous type passed to the method is there in order to make the anonymous type a compile-time Type, the method doesn't care what the values are that you pass in for it.) :

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

Which produces:

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 }
Ian Mercer
  • 35,804
  • 6
  • 87
  • 121
  • I totally didn't get this answer. So far option # 1 from Klaus Gütter does seem to be the way to go even if it's not quite dynamic – Nick Farsi Aug 30 '20 at 17:14
0

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.

Nick Farsi
  • 164
  • 1
  • 8