Short question:
Is there a simple way in LINQ to objects to get a distinct list of objects from a list based on a key property on the objects.
This idea is being debated here, and while I'm hoping the .NET Core team adopt a method to generate IEqualityComparer<T>
s from lambda, I'd suggest you to please vote and comment on that idea, and use the following:
Usage:
IEqualityComparer<Contact> comp1 = EqualityComparerImpl<Contact>.Create(c => c.Name);
var comp2 = EqualityComparerImpl<Contact>.Create(c => c.Name, c => c.Age);
class Contact { public Name { get; set; } public Age { get; set; } }
Code:
public class EqualityComparerImpl<T> : IEqualityComparer<T>
{
public static EqualityComparerImpl<T> Create(
params Expression<Func<T, object>>[] properties) =>
new EqualityComparerImpl<T>(properties);
PropertyInfo[] _properties;
EqualityComparerImpl(Expression<Func<T, object>>[] properties)
{
if (properties == null)
throw new ArgumentNullException(nameof(properties));
if (properties.Length == 0)
throw new ArgumentOutOfRangeException(nameof(properties));
var length = properties.Length;
var extractions = new PropertyInfo[length];
for (int i = 0; i < length; i++)
{
var property = properties[i];
extractions[i] = ExtractProperty(property);
}
_properties = extractions;
}
public bool Equals(T x, T y)
{
if (ReferenceEquals(x, y))
//covers both are null
return true;
if (x == null || y == null)
return false;
var len = _properties.Length;
for (int i = 0; i < _properties.Length; i++)
{
var property = _properties[i];
if (!Equals(property.GetValue(x), property.GetValue(y)))
return false;
}
return true;
}
public int GetHashCode(T obj)
{
if (obj == null)
return 0;
var hashes = _properties
.Select(pi => pi.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
return Combine(hashes);
}
static int Combine(int[] hashes)
{
int result = 0;
foreach (var hash in hashes)
{
uint rol5 = ((uint)result << 5) | ((uint)result >> 27);
result = ((int)rol5 + result) ^ hash;
}
return result;
}
static PropertyInfo ExtractProperty(Expression<Func<T, object>> property)
{
if (property.NodeType != ExpressionType.Lambda)
throwEx();
var body = property.Body;
if (body.NodeType == ExpressionType.Convert)
if (body is UnaryExpression unary)
body = unary.Operand;
else
throwEx();
if (!(body is MemberExpression member))
throwEx();
if (!(member.Member is PropertyInfo pi))
throwEx();
return pi;
void throwEx() =>
throw new NotSupportedException($"The expression '{property}' isn't supported.");
}
}
You could group by the key value and then select the top item from each group. Would that work for you?
This is the best i can come up with for the problem in hand.
Still curious whether theres a nice way to create a EqualityComparer
on the fly though.
Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First());
Create lookup table and take 'top' from each one
Note: this is the same as @charlie suggested but using ILookup - which i think is what a group must be anyway.
implement IEquatable on GalleryImage because it is generated
A different approach would be to generate GalleryImage as a partial class, and then have another file with the inheritance and IEquatable, Equals, GetHash implementation.
Building on Charlie Flowers' answer, you can create your own extension method to do what you want which internally uses grouping:
public static IEnumerable<T> Distinct<T, U>(
this IEnumerable<T> seq, Func<T, U> getKey)
{
return
from item in seq
group item by getKey(item) into gp
select gp.First();
}
You could also create a generic class deriving from EqualityComparer, but it sounds like you'd like to avoid this:
public class KeyEqualityComparer<T,U> : IEqualityComparer<T>
{
private Func<T,U> GetKey { get; set; }
public KeyEqualityComparer(Func<T,U> getKey) {
GetKey = getKey;
}
public bool Equals(T x, T y)
{
return GetKey(x).Equals(GetKey(y));
}
public int GetHashCode(T obj)
{
return GetKey(obj).GetHashCode();
}
}
What about a throw away IEqualityComparer
generic class?
public class ThrowAwayEqualityComparer<T> : IEqualityComparer<T>
{
Func<T, T, bool> comparer;
public ThrowAwayEqualityComparer(Func<T, T, bool> comparer)
{
this.comparer = comparer;
}
public bool Equals(T a, T b)
{
return comparer(a, b);
}
public int GetHashCode(T a)
{
return a.GetHashCode();
}
}
So now you can use Distinct
with a custom comparer.
var distinctImages = allImages.Distinct(
new ThrowAwayEqualityComparer<GalleryImage>((a, b) => a.Key == b.Key));
You might be able to get away with the <GalleryImage>
, but I'm not sure if the compiler could infer the type (don't have access to it right now.)
And in an additional extension method:
public static class IEnumerableExtensions
{
public static IEnumerable<TValue> Distinct<TValue>(this IEnumerable<TValue> @this, Func<TValue, TValue, bool> comparer)
{
return @this.Distinct(new ThrowAwayEqualityComparer<TValue>(comparer);
}
private class ThrowAwayEqualityComparer...
}