1
votes

C # LINQ, regroupement dynamique par attributs [Key]

Considérez les classes suivantes:

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

Chaque classe a des propriétés avec l'attribut [Key]. Est-il possible de regrouper dynamiquement un IEnumerable de l'une de ces classes par leurs attributs [Key] respectifs?


3 commentaires

Qu'entendez-vous par «dynamiquement»? Pour construire l'expression GroupBy en utilisant la réflexion? Pourquoi voudriez-vous faire ça?


Oui, c'est ce que je recherche. Parce qu'alors je pourrais faire quelque chose comme .GroupByKey () sans avoir à spécifier des clés pour chaque classe que j'ai


Avez-vous vraiment besoin d'expressions GroupBy dynamiques (pour IQueryable)? Si tel est le cas, Ian Mercer a répondu à cela. Les réponses de Klaus Gütters ne sont pas dynamiques ou pas pour IQueryable. Je ne sais pas ce dont vous avez besoin.


3 Réponses :


2
votes

Je préférerais ajouter des méthodes d'extension pour chacun de vos types, comme

Option 1:

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

S'il y a beaucoup de types , vous pouvez générer le code en utilisant t4 .

Utilisation: .GroupByPrimaryKey().

Option 2:

Une variante plus simple:

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

}

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

Option 3:

Une solution avec réflexion est possible, mais sera lente. Sketch (loin d'être prêt pour la production!)

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

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


2 commentaires

Je pense que vous constaterez que LINQ to SQL ne prend pas en charge Tuple.Create et que vous devez utiliser des types anonymes.


@ianmercer L'OP n'a pas précisé qu'il s'agissait de Linq-To-Sql, mais si c'est le cas, vous avez probablement raison.



1
votes

Le défi ici est que vous devez créer un type anonyme afin d'avoir une expression GroupBy qui peut se traduire en SQL ou tout autre fournisseur LINQ.

Je ne suis pas sûr que vous peut le faire en utilisant la réflexion (non sans un code vraiment complexe pour créer un type anonyme au moment de l'exécution). Mais vous pouvez créer l'expression de regroupement si vous étiez prêt à fournir un exemple du type anonyme comme germe.

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 }

Et voici comment vous l'utiliseriez (Remarque: le mannequin le type anonyme passé à la méthode est là pour faire du type anonyme un type à la compilation, la méthode ne se soucie pas des valeurs que vous lui transmettez.):

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


1 commentaires

Je n'ai absolument pas eu cette réponse. Jusqu'à présent, l'option n ° 1 de Klaus Gütter semble être la voie à suivre même si elle n'est pas assez dynamique



0
votes

Quelqu'un a publié une réponse valide et l'a supprimée plus tard pour une raison quelconque. La voici:

Classe de clé combinée:

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

Fonction de l'appelant et utilisation réelle:

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

Oui, il est relativement lent, donc si c'est un problème, alors les solutions de Klaus Gütter sont la voie à suivre.


0 commentaires