letie
letie

Reputation: 894

How to .GroupBy() by Id and by list property?

I have these classes:

public class AlertEvaluation
{
    public string AlertId { get; set; }
    public ICollection<EvaluatedTag> EvaluatedTags { get; set; }
    public string TransactionId { get; set; }
    public EvaluationStatus EvaluationStatus { get; set; }
    public DateTime EvaluationDate { get; set; }
}

public class EvaluatedTag
{
    public string Id { get; set; }
    public string Name { get; set; }
}

And I would like to get a list of alert evaluations grouped by AlertId, and by EvaluatedTags, meaning that I would like to compare and group evaluations that not only have the same AlertId, but to also have the same list of EvaluatedTags. (And also get the last evaluation in time)

I tried this:

var evaluationsGroupedAndOrdered = evaluations.GroupBy(x => new { x.AlertSettingId, x.EvaluatedLabels })
.Select(x => x.OrderByDescending(z => z.EvaluationDate ).FirstOrDefault()).ToList();

But of course, the comparing of list properties like that did not work.

I read something about adding an equality comparer in GroupBy, which would mean comparing the lists inside the objects right? But I'm not sure of how to implement it in the right way.

I tried (based on GroupBy on complex object (e.g. List<T>)) :

        public class AlertEvaluationComparer : IEqualityComparer<AlertEvaluation>
    {
        public bool Equals(AlertEvaluation x, AlertEvaluation y)
        {
            return x.AlertId == y.AlertId && x.EvaluatedTags.OrderBy(val => val.Name).SequenceEqual(y.EvaluatedTags.OrderBy(val => val.Name));
        }

        public int GetHashCode(AlertSettingEvaluation x)
        {
            return x.AlertId.GetHashCode() ^ x.EvaluatedTags.Aggregate(0, (a, y) => a ^ y.GetHashCode());
        }
    }

But did not work either.. Maybe because my list EvaluatedTags is not a list of strings but of individual objects.

Does anybody have a nice solution for this?

Upvotes: 1

Views: 562

Answers (2)

Rufus L
Rufus L

Reputation: 37030

A typical way to compare two lists is to use the System.Linq exension method, SequenceEquals. This method returns true if both lists contain the same items, in the same order.

In order to make this work with an IEnumerable<EvaluatedTag>, we need to have a way to compare instances of the EvaluatedTag class for equality (determining if two items are the same) and for sorting (since the lists need to have their items in the same order).

To do this, we can override Equals and GetHashCode and implement IComparable<EvaluatedTag> (and might as well do IEquatable<EvaluatedTag> for completeness):

public class EvaluatedTag : IEquatable<EvaluatedTag>, IComparable<EvaluatedTag>
{
    public string Id { get; set; }
    public string Name { get; set; }

    public int CompareTo(EvaluatedTag other)
    {
        if (other == null) return -1;
        var result = string.CompareOrdinal(Id, other.Id);
        return result == 0 ? string.CompareOrdinal(Name, other.Name) : result;
    }

    public bool Equals(EvaluatedTag other)
    {
        return other != null &&
               string.Equals(other.Id, Id) &&
               string.Equals(other.Name, Name);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as EvaluatedTag);
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode() * 17 +
               Name.GetHashCode() * 17;
    }
}

Now we can use this in the custom comparer you have in your question, for sorting and comparing the EvaluatedTags:

public class AlertEvaluationComparer : IEqualityComparer<AlertEvaluation>
{
    // Return true if the AlertIds are equal, and the EvaluatedTags 
    // contain the same items (call OrderBy to ensure they're in 
    // the same order before calling SequenceEqual).
    public bool Equals(AlertEvaluation x, AlertEvaluation y)
    {
        if (x == null) return y == null;
        if (y == null) return false;
        if (!string.Equals(x.AlertId, y.AlertId)) return false;
        if (x.EvaluatedTags == null) return y.EvaluatedTags == null;
        if (y.EvaluatedTags == null) return false;
        return x.EvaluatedTags.OrderBy(et => et)
            .SequenceEqual(y.EvaluatedTags.OrderBy(et => et));
    }

    // Use the same properties in GetHashCode that were used in Equals
    public int GetHashCode(AlertEvaluation obj)
    {
        return obj.AlertId?.GetHashCode() ?? 0 * 17 +
               obj.EvaluatedTags?.Sum(et => et.GetHashCode() * 17) ?? 0;
    }
}

And finally we can pass your AlertEvaluationComparer to the GroupBy method to group our items:

var evaluationsGroupedAndOrdered = evaluations
    .GroupBy(ae => ae, new AlertEvaluationComparer())
    .OrderBy(group => group.Key.EvaluationDate)
    .ToList();

Upvotes: 1

Noah Stahl
Noah Stahl

Reputation: 7603

Here's a go at it, getting away from Linq a bit to make it easier to build the groups one at a time while leveraging sorting:

// Build groups by using a combination of AlertId and EvaluatedTags hashcode as group key
var groupMap = new Dictionary<string, SortedSet<AlertEvaluation>>();
foreach (var item in evals)
{
    var combinedKey = item.AlertId + EvaluatedTag.GetCollectionHashCode(item.EvaluatedTags);
    if (groupMap.TryGetValue(combinedKey, out SortedSet<AlertEvaluation>? groupItems))
    {
        // Add to existing group
        groupItems.Add(item);
    }
    else
    {
        // Create new group
        groupMap.Add(combinedKey, new SortedSet<AlertEvaluation> { item });
    }
}

// Get a list of groupings already sorted ascending by EvaluationDate
List<SortedSet<AlertEvaluation>>? groups = groupMap.Values.ToList();

This assumes that the classes implement IComparable and Equals/GetHashCode to facilitate sorting:

public class AlertEvaluation : IComparable<AlertEvaluation>
{
    public string AlertId { get; set; }
    public ICollection<EvaluatedTag> EvaluatedTags { get; set; }
    public string TransactionId { get; set; }
    public EvaluationStatus EvaluationStatus { get; set; }
    public DateTime EvaluationDate { get; set; }

    // Used by SortedSet
    public int CompareTo(AlertEvaluation? other)
    {
        if (other is null)
        {
            return 1;
        }

        return EvaluationDate.CompareTo(other.EvaluationDate);
    }
}

public class EvaluatedTag : IEquatable<EvaluatedTag?>
{
    public string Id { get; set; }
    public string Name { get; set; }

    public bool Equals(EvaluatedTag? other) => other != null && Id == other.Id && Name == other.Name;

    public override int GetHashCode() => HashCode.Combine(Id, Name);

    // Helper to get a hash of item collection
    public static int GetCollectionHashCode(ICollection<EvaluatedTag> items)
    {
        var code = new HashCode();
        foreach (var item in items.OrderBy(i => i.Id))
        {
            code.Add(item);
        }
        return code.ToHashCode();
    }
}

By the way, I'm using the fancy new HashCode class in .NET Core to override hash codes.

Upvotes: 1

Related Questions