LINQ GroupBy on multiple ref-type fields; Custom EqualityComparer
-
13-12-2019 - |
Question
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.
- How can I specify custom code to compare the GroupBy fields? For example, the Components could be compared by Component.Code.
Solution
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
andComponentDto
. If there's a natural equality you'd want to use everywhere, this is probably the sanest option. I'd recommend implementingIEquatable<T>
as well - Don't use an anonymous type for grouping - use a named type, and then you can either override
GetHashCode
andEquals
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.
OTHER TIPS
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)