IEnumerable.Except() between different classes with a common field
Asked Answered
T

6

6

Is it possible to use Except() for two List's that have two different classes but a common field? I have List<User1> and List<User2> collections. They have different properties except Id column and I want to find the different records between them using this Id column. I'm trying to use List<>.Except() but I'm getting this error:

The type arguments for method 'System.Linq.Enumerable.Except(System.Collections.Generic.IEnumerable, System.Collections.Generic.IEnumerable)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

Here's what I'm trying:

List<User1> list1 = List1();
List<User2> list2 = List2();
var listdiff = list1.Except(list2.Select(row => row.Id));

What am I doing wrong?

Thisbe answered 1/11, 2010 at 15:59 Comment(1)
Please read this famous blog to understand the internals of Enumerable.Except() method.Outlander
L
9

List1 contains instances of User1 and List2 contains instances of User2.

What type of instance should be produced by list1.Except(list2.Select(row => row.Id))? In other words if type inference was not available, what would you replace var with?

If User1 and User2 inherit from the same ancestor (with ID), use List<User> instead.

Otherwise:

var list2Lookup = list2.ToLookup(user => user.Id);
var listdiff = list1.Where(user => (!list2Lookup.Contains(user.Id))
Lacrimatory answered 1/11, 2010 at 16:10 Comment(7)
+1 if you make the lookup on list2 instead of list1. Or without the lookup var listdiff = list1.Where(user => !(list2.Any(user2 => user2.Id == user.Id));Itagaki
Yup noticed that mistake a couple of minutes agoLacrimatory
Note that this doesn't have exactly the same semantics as Except. Except returns the "set difference" of the two sequences; that is, only unique items from the first sequence are returned (and, obviously, only returned if they're not in the second sequence). msdn.microsoft.com/en-us/library/…Boarding
Yes, the problem here is that the sequences are heterogeneous so we need to get down to a 'common ground'Lacrimatory
Excellent. I was looking for using Except() but it seems you can't use it if you don't have a common ground for both classes. I'd like to know why you chose to use LookUp<> though?Thisbe
To speed up the check whether list2 also contains the current element (list2Lookup.Contains(user.Id)).Lacrimatory
Worked great for me, cheers guys! Still marveling at the power of LINQ when you properly bend it to your will :-)Cychosz
B
4

Not Except, but the correct results and similar performance:

// assumes that the Id property is an Int32
var tempKeys = new HashSet<int>(list2.Select(x => x.Id));
var listdiff = list1.Where(x => tempKeys.Add(x.Id));

And, of course, you can wrap it all up in your own re-usable extension method:

var listdiff = list1.Except(list2, x => x.Id, y => y.Id);

// ...

public static class EnumerableExtensions
{
    public static IEnumerable<TFirst> Except<TFirst, TSecond, TKey>(
        this IEnumerable<TFirst> first,
        IEnumerable<TSecond> second,
        Func<TFirst, TKey> firstKeySelector,
        Func<TSecond, TKey> secondKeySelector)
    {
        // argument null checking etc omitted for brevity

        var keys = new HashSet<TKey>(second.Select(secondKeySelector));
        return first.Where(x => keys.Add(firstKeySelector(x)));
    }
}
Boarding answered 1/11, 2010 at 16:39 Comment(0)
E
3

Briefly, make lists to be List<object> and use C# feature from .NET 4.0: dynamic.

Example:

var listDiff = list1
    .AsEnumerable<object>()
    .Except(list2
        .AsEnumerable<object>()
        .Select(row => ((dynamic)row).ID));
Eyewash answered 1/11, 2010 at 16:3 Comment(1)
Sorry forgot to add that I'm using v3.5Thisbe
O
2

If you just want the Ids in list1 that are not in list2, you can do:

var idsInList1NotInList2 = list1.Select(user1 => user1.Id)
                                .Except(list2.Select(user2 => user2.Id));

If you need the associated User1 objects too, here's one way (assuming Ids are unique for a User1 object):

// Create lookup from Id to the associated User1 object
var user1sById = list1.ToDictionary(user1 => user1.Id);

// Find Ids from the lookup that are not present for User2s from list2
// and then retrieve their associated User1s from the lookup
var user1sNotInList2 = user1sById.Keys
                                 .Except(list2.Select(user2 => user2.Id))
                                 .Select(key => user1sById[key]);

EDIT: vc74's take on this idea is slightly better; it doesn't require uniqueness.

Onagraceous answered 1/11, 2010 at 16:8 Comment(0)
C
2
public static IEnumerable<TSource> Except<TSource, CSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> TSelector, IEnumerable<CSource> csource, Func<CSource, TKey> CSelector)
    {
        bool EqualFlag = false;
        foreach (var s in source)
        {
            EqualFlag = false;
            foreach (var c in csource)
            {
                var svalue = TSelector(s);
                var cvalue = CSelector(c);
                if (svalue != null)
                {

                    if (svalue.Equals(cvalue))
                    {
                        EqualFlag = true;
                        break;
                    }
                }
                else if (svalue == null && cvalue == null)
                {
                    EqualFlag = true;
                    break;
                }
            }
            if (EqualFlag)
                continue;
            else
            {
                yield return s;
            }
        }

    }
Chenoweth answered 1/3, 2016 at 9:18 Comment(0)
S
0

Try

list1.Where(user1 => !list2.Any(user2 => user2.Id.Equal(user1.Id)));
Secondhand answered 10/8, 2017 at 13:35 Comment(1)
Welcome to Stack Overflow! Why does this work? Those who can answer that question for themselves probably don't need someone else's code.Troupe

© 2022 - 2024 — McMap. All rights reserved.