2

So I've looked through about 20 examples on this on SO and elsewhere, but haven't found one which covers what I'm trying to do. This - Can I specify my explicit type comparator inline? - looks like what I need, but doesn't go far enough (or I don't understand how to take it further).

  • I have a List of LoadData, the LoadData object has fields of both reference and value types
  • Need to group on a mixture of ref and value fields, project the output to an anonymous type
  • Need (I think) to provide a custom IEqualityComparer to specify how to compare the GroupBy fields, but they are an anonymous type

    private class LoadData
    {
        public PeriodEndDto PeriodEnd { get; set; }
        public ComponentDto Component { get; set; }
        public string GroupCode { get; set; }
        public string PortfolioCode { get; set; }
    }
    

The best GroupBy query I have going so far:

var distinctLoads = list.GroupBy(
    dl => new { PeriodEnd = dl.PeriodEnd, 
                Component = dl.Component, 
                GroupCode = dl.GroupCode },
    (key, data) => new {PeriodEnd = key.PeriodEnd, 
                Component = key.Component, 
                GroupCode = key.GroupCode, 
                PortfolioList = data.Select(d=>d.PortfolioCode)
                                    .Aggregate((g1, g2) => g1 + "," + g2)},
    null);

This groups, but there are still duplicates.

  1. How can I specify custom code to compare the GroupBy fields? For example, the Components could be compared by Component.Code.
Community
  • 1
  • 1
Peter
  • 5,455
  • 7
  • 46
  • 68

2 Answers2

8

The problem here is that your key type is anonymous, which means you can't declare a class that implements IEqualityComparer<T> for that key type. While it would be possible to write a comparator which compared anonymous types for equality in a custom manner (via a generic method, delegates and type inference), it wouldn't be terribly pleasant.

The two simplest options are probably:

  • Make the anonymous type "just work" by overriding Equals/GetHashCode in PeriodEndDto and ComponentDto. If there's a natural equality you'd want to use everywhere, this is probably the sanest option. I'd recommend implementing IEquatable<T> as well
  • Don't use an anonymous type for grouping - use a named type, and then you can either override GetHashCode and Equals on that, or you could write a custom equality comparer in the normal way.

EDIT: ProjectionEqualityComparer wouldn't really work. It would be feasible to write something similar though - a sort of CompositeEqualityComparer which allowed you create an equality comparer from several "projection + comparer" pairs. It would be pretty ugly compared with the anonymous type though.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Hi @Jon - thank you, I went the first route and it solved my problem, nice and easy. A couple of questions, though - by overriding Equals/GetHashCode I'm essentially telling the code which uses these objects that two different instances are equal based on the data they hold. Is there any risk to this - ie. anywhere the .NET Framework depends on these methods to know that two objects are different instances? And secondly, is there anywhere with further examples of how to use your ProjectionEqualityComparer? I'd like to get a better grip on how it works. – Peter Oct 03 '12 at 11:32
  • Anywhere that checks for equality - dictionaries, `Contains` calls etc. If there's a natural idea of equality, that's probably going to be a good thing for all code. – Jon Skeet Oct 03 '12 at 11:34
3

EDIT:

As Jon Skeet points out, this solution seems better than it is, if you don't think too hard about it, because I have forgotten to implement GetHashCode. Having to implement GetHashCode makes this approach, as Jon says in his answer, "not terribly pleasant." Presumably, this is also the explanation for the (so-called "inexplicable") absence of EqualityComparer<T>.Create() in the framework. I'll leave the answer for reference, as examples of what not to do, can be instructive as well.

ORIGINAL ANSWER:

You could use the approach suggested by the Comparer<T>.Create pattern introduced in .NET 4.5 (but inexplicably absent in EqualityComparer<T>). To do so, create a DelegateEqualityComparer<T> class:

class DelegateEqualityComparer<T> : EqualityComparer<T>
{
    private readonly Func<T, T, bool> _equalityComparison;

    private DelegateEqualityComparer(Func<T, T, bool> equalityComparison)
    {
        if (equalityComparison == null)
            throw new ArgumentNullException("equalityComparison");
        _equalityComparison = equalityComparison;
    }

    public override bool Equals(T x, T y)
    {
        return _equalityComparison(x, y);
    }

    public static DelegateEqualityComparer<T> Create(
        Func<T, T, bool> equalityComparison)
    {
        return new DelegateEqualityComparer<T>(equalityComparison);
    }
}

Then write wrappers around the GroupBy methods to accept a Func<TKey, TKey, bool> delegate in place of the IEqualityComparer<TKey> parameter. These methods wrap the delegate in a DelegateEqualityComparer<T> instance, and pass that on to the corresponding GroupBy method. Example:

public static class EnumerableExt
{
    public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> keySelector,
        Func<TKey, TKey, bool> equalityComparison)
    {
        return source.GroupBy(
            keySelector,
            DelegateEqualityComparer<TKey>.Create(equalityComparison);
    }
}

Finally, at your call site, you would use something like this expression for the equalityComparison argument:

(a, b) => a.PeriodEnd.Equals(b.PeriodEnd)
    && a.Component.Code.Equals(b.Component.Code)
    && a.GroupCode.Equals(b.GroupCode)
phoog
  • 42,068
  • 6
  • 79
  • 117
  • 1
    You haven't implemented GetHashCode, so this wouldn't compile yet. – Jon Skeet Oct 03 '12 at 11:35
  • @phoog - thanks, Jon's post solved my immediate problem, but I'm going to dig around with this a bit later to understand the GroupBy better. – Peter Oct 03 '12 at 11:38
  • @JonSkeet Note that I am deriving from `EqualityComparer`, not `IEqualityComparer`, so my failure to override virtual `GetHashCode` is a logic error, not a compilation error. Thanks for pointing it out! Why does the framework even allow EqualityComparer to be subclassed? I was wondering this when I posted the above, but now that I see how it makes it *easier to forget to implement GetHashCode(),* I am doubly curious. – phoog Oct 03 '12 at 16:35
  • 1
    @phoog: `EqualityComparer.GetHashCode(T)` is an abstract method though - so it should *still* be a compilation error. – Jon Skeet Oct 03 '12 at 16:38
  • ah, you're right (blush). I did write the sample code in VS, but I must have done it while I had ReSharper disabled. I've added a caveat/retraction to the beginning of the answer. – phoog Oct 03 '12 at 16:51