Improving LINQ Except and Intersect by using a lambda expression for custom comparison
I was recently doing some work where I had to use Except and Insersect, but with some custom comparison logic. Let me illustrate by way of an example. If I had the following class
public class Widget
{
public int ID { get; set; }
public string Name { get; set; }
}
and created two lists like this
List<Widget> first = new List<Widget>()
{
new Widget() { ID = 1, Name = "Alpha"},
new Widget() { ID = 2, Name = "Beta"},
new Widget() { ID = 3, Name = "Gamma"},
};
List<Widget> second = new List<Widget>()
{
new Widget() { ID = 2, Name = "Foo"},
new Widget() { ID = 3, Name = "Bar"},
new Widget() { ID = 4, Name = "Baz"},
};
and tried to use Except like this
first.Except(second).ToList().ForEach(x => Console.WriteLine("ID=" + x.ID + ", " + x.Name));
I would get:
ID=1, Alpha
ID=2, Beta
ID=3, Gamma
Since I wanted to do a comparison on the ID property, what I really want is just the first item the list i.e. [ID=1, Alpha]. One way of achieving this to override Equals() on the Widget class, but this may not always be practical or even possible. Another option is to supply a custom IEqualityComparer<T>. To do this I would need to a define a class that implements Equals() and GetHashCode(), the two methods on IEqualityComparer<T> and supply an instance of this class as an additional parameter to Equals() or Intersect(). Unfortunately, I found neither of these practical since I had a number of classes that that I wanted to use with Except() or Intersect() and wanted a more elegant solution.
“Why not just use a lambda expression to express the comparison?”, I thought and came up with the following two extensions:
public static class LinqExtensions
{
public static IEnumerable<TSource> Except<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TSource, bool> comparer)
{
return first.Where(x => second.Count(y => comparer(x, y)) == 0);
}
public static IEnumerable<TSource> Intersect<TSource>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TSource, bool> comparer)
{
return first.Where(x => second.Count(y => comparer(x, y)) == 1);
}
}
Now I can compare on the ID by supplying the comparison logic as a lambda expression:
Console.WriteLine("Except");
first.Except(second, (x, y) => x.ID == y.ID).ToList().ForEach(x => Console.WriteLine("ID=" + x.ID + ", " + x.Name));
Console.WriteLine("Intersect");
first.Intersect(second, (x, y) => x.ID == y.ID).ToList().ForEach(x => Console.WriteLine("ID=" + x.ID + ", " + x.Name));
This gives me the results I want:
Except
ID=1, Alpha
Intersect
ID=2, Beta
ID=3, Gamma
Hope you find them useful!