Question

LINQ List Difference in C#: Find Items Not in Another List

csharplinq.net-3.5

Question

Given two List<Person> collections in C#, how can I use LINQ to return every person in peopleList2 whose ID does not appear in peopleList1?

class Program
{
    static void Main(string[] args)
    {
        List<Person> peopleList1 = new List<Person>
        {
            new Person { ID = 1 },
            new Person { ID = 2 },
            new Person { ID = 3 }
        };

        List<Person> peopleList2 = new List<Person>
        {
            new Person { ID = 1 },
            new Person { ID = 2 },
            new Person { ID = 3 },
            new Person { ID = 4 },
            new Person { ID = 5 }
        };
    }
}

class Person
{
    public int ID { get; set; }
}

The result should contain the people with ID values 4 and 5.

Short Answer

You will learn how to compare two C# collections by a property and find items that exist only in one list. You will also see why comparing object instances directly can produce unexpected results, and when HashSet, Where, and Except are appropriate.

Concept

LINQ can filter a sequence based on whether a condition is true. To find people in peopleList2 that do not exist in peopleList1, compare each person's ID with the IDs in the first list.

The important detail is that the two lists contain separate Person objects. Even if two objects both have ID = 1, they are different object instances. By default, a normal class uses reference equality: two objects are equal only when they refer to the exact same object in memory.

Therefore, this will not reliably compare people by ID:

var missingPeople = peopleList2.Except(peopleList1);

Instead, explicitly compare the key that identifies a person, such as ID. For efficient lookups, first place the IDs from the first list in a HashSet<int>, then filter the second list.

This pattern is useful whenever one collection contains records already processed, saved, authorized, or synchronized, and you need to locate records that are new.

Mental Model

Think of peopleList1 as a guest list containing badge numbers 1, 2, and 3. peopleList2 is a line of people with badge numbers 1 through 5.

To find who is not on the guest list, inspect each person in the line and ask: “Is this badge number on the list?” If the answer is no, keep that person.

A HashSet<int> is like a reception desk that can check badge numbers very quickly. The set stores only the known IDs, while the filtered result retains the complete Person objects from peopleList2.

Take Quiz

Syntax and Examples

Use Where to keep items that satisfy a condition, and Contains to test whether an ID is known.

using System.Collections.Generic;
using System.Linq;

var existingIds = peopleList1.Select(person => person.ID).ToHashSet();

List<Person> missingPeople = peopleList2
    .Where(person => !existingIds.Contains(person.ID))
    .ToList();

Select(person => person.ID) extracts the IDs from the first list. ToHashSet() stores those IDs for fast membership checks.

Then:

  • Where(...) examines every person in peopleList2.
  • existingIds.Contains(person.ID) is true when that ID is in the first list.
  • ! means “not”, so only people whose IDs are absent are retained.
  • ToList() executes the query and creates a List<Person>.

For the example, missingPeople contains people with IDs 4 and 5.

A compact alternative is valid for small collections:

var missingPeople = peopleList2
    .Where(person2 => !peopleList1.Any(person1 => person1.ID == person2.ID))
    .ToList();

However, the HashSet approach is usually a better choice when the lists may be large.

Step by Step Execution

Consider this code:

var existingIds = peopleList1.Select(person => person.ID).ToHashSet();

var missingPeople = peopleList2
    .Where(person => !existingIds.Contains(person.ID))
    .ToList();
  1. peopleList1.Select(person => person.ID) produces the values 1, 2, and 3.
  2. ToHashSet() creates a set containing { 1, 2, 3 }.
  3. LINQ checks each person in peopleList2:
    • Person 1: the set contains 1, so !true is false; exclude it.
    • Person 2: the set contains 2; exclude it.
    • Person 3: the set contains 3; exclude it.
    • Person 4: the set does not contain 4, so !false is true; include it.
    • Person 5: the set does not contain 5; include it.
  4. ToList() materializes the filtered sequence.
  5. The final list contains IDs 4 and 5.

LINQ uses deferred execution: the Where query does not enumerate peopleList2 until it is iterated or materialized by a method such as ToList().

Real World Use Cases

Common uses of this collection-difference pattern include:

  • Database synchronization: find records received from an API that are not already stored locally.
  • User management: find requested user IDs that are not members of a team or role.
  • Import tools: identify rows in a CSV file that have not previously been imported.
  • Notifications: send alerts only to users who have not already received a notification.
  • Inventory: identify products in a supplier feed that do not yet exist in the catalog.
  • Permissions: find requested permissions that are not included in a user's current permission set.

In each case, compare a stable identifier such as an integer ID, GUID, email address, or database key rather than comparing entire objects accidentally.

Take Quiz

Real Codebase Usage

In production code, developers commonly build a lookup set once and reuse it while filtering.

var existingCustomerIds = existingCustomers
    .Select(customer => customer.Id)
    .ToHashSet();

var customersToCreate = incomingCustomers
    .Where(customer => !existingCustomerIds.Contains(customer.Id))
    .ToList();

This is clear and efficient: building the set is approximately proportional to the size of the existing collection, and each lookup is typically fast.

For input validation, combine filtering with guard clauses:

if (incomingCustomers is null)
{
    throw new ArgumentNullException(nameof(incomingCustomers));
}

var existingIds = existingCustomers.Select(x => x.Id).ToHashSet();
var newCustomers = incomingCustomers.Where(x => !existingIds.Contains(x.Id));

When querying a database through an ORM such as Entity Framework, keep the comparison in the query when possible so the database performs the filtering:

var existingIds = request.CustomerIds.ToList();

var customersNotRequested = dbContext.Customers
    .Where(customer => !existingIds.Contains(customer.Id))
    .ToList();

The exact best approach depends on whether the collections are already in memory or are database queries.

Common Mistakes

Using Except directly on ordinary class objects

This looks reasonable but usually gives the wrong result:

var missingPeople = peopleList2.Except(peopleList1).ToList();

Because each list creates different Person instances, default equality sees every item in peopleList2 as different from every item in peopleList1. The result may incorrectly include IDs 1 through 5.

Avoid it: compare IDs with Where and Contains, use a custom comparer, or define value equality.

Comparing the wrong property

.Where(person => !existingIds.Contains(person.Age))

An age is usually not a unique identifier. Multiple people can have the same age.

Avoid it: compare a unique, stable key such as ID.

Repeatedly scanning the first list for large inputs

var missingPeople = peopleList2
    .Where(p2 => !peopleList1.Any(p1 => p1.ID == p2.ID))
    .ToList();

This is correct, but for every item in peopleList2, it may scan peopleList1. It can become slow with large lists.

Avoid it: create a HashSet<int> of IDs first.

Forgetting ToList() when a list is required

var missingPeople = peopleList2.Where(person => !existingIds.Contains(person.ID));

This produces an IEnumerable<Person>, not a List<Person>.

Avoid it: add .ToList() only when you need a concrete list immediately. Keeping it as IEnumerable<Person> is also fine when you only plan to enumerate it.

Assuming IDs are unique when they are not

If peopleList2 contains two people with ID 4, both will be returned. Filtering does not remove duplicates.

Avoid it: enforce unique IDs in your data, or use an additional deduplication step when that is the intended behavior.

Comparisons

ApproachExampleBest useNotes
Where + HashSet.ContainspeopleList2.Where(p => !ids.Contains(p.ID))In-memory lists, especially large onesClear and typically efficient
Where + AnypeopleList2.Where(p => !peopleList1.Any(x => x.ID == p.ID))Small lists and simple one-off codeCan repeatedly scan the first list
Except on IDspeopleList2.Select(p => p.ID).Except(peopleList1.Select(p => p.ID))When only the missing IDs are neededReturns IDs, not Person objects
Except with comparerpeopleList2.Except(peopleList1, comparer)Reusable object comparison rulesRequires an IEqualityComparer<Person>
Join / GroupJoinMatch items by a key across collectionsWhen results need data from both sidesMore verbose; useful for richer matching

If you only need missing ID values, Except is concise:

var missingIds = peopleList2
    .Select(person => person.ID)
    .Except(peopleList1.Select(person => person.ID))
    .ToList();

If you need the complete Person objects from the second list, use Where with a set of IDs.

Take Quiz

Cheat Sheet

// Best general in-memory pattern: keep objects from the second list
var knownIds = list1.Select(x => x.ID).ToHashSet();
var onlyInList2 = list2.Where(x => !knownIds.Contains(x.ID)).ToList();

// Simple alternative for small lists
var onlyInList2 = list2
    .Where(x => !list1.Any(y => y.ID == x.ID))
    .ToList();

// When only IDs are needed
var idsOnlyInList2 = list2.Select(x => x.ID)
    .Except(list1.Select(x => x.ID))
    .ToList();
  • Compare a unique key such as ID.
  • ! means “not”.
  • Where returns matching original objects.
  • Select transforms objects into another value, such as IDs.
  • ToHashSet() is useful for repeated membership checks.
  • ToList() creates a concrete list and executes a deferred LINQ query.
  • Do not use Except directly on class instances unless their equality behavior matches your intended comparison.

FAQ

How do I find items in one C# list that are not in another?

Extract the IDs from the first list into a HashSet, then use Where to retain items in the second list whose IDs are not in that set.

Why does Except not work with my Person class?

By default, classes use reference equality. Two separately created Person objects are not equal merely because their ID properties have the same value.

Should I use Any or HashSet.Contains?

Use Any for small collections or very simple code. Use HashSet.Contains when filtering many items or when performance matters.

Does this modify either original list?

No. LINQ filtering creates a new sequence. Calling ToList() creates a new list containing references to the selected Person objects.

How can I get only the IDs that are missing?

Use Select and Except:

var missingIds = peopleList2.Select(x => x.ID)
    .Except(peopleList1.Select(x => x.ID));

What happens if the second list has duplicate IDs?

A Where query returns every matching object, including duplicates. Except returns distinct values.

Can I compare by more than one property?

Yes. For a simple filter, compare both properties in Any, or create a composite key such as an anonymous type or tuple when appropriate.

Take Quiz

Mini Project

Description

Build a small import filter for a contact-management application. The application receives people from an external source and must identify the people who are not already in its existing contact list. This demonstrates comparing objects by an ID rather than by object reference.

Goal

Print the IDs of incoming people who are not already present in the existing people list.

Requirements

Create an existing people list containing IDs 1, 2, and 3. Create an incoming people list containing IDs 2, 3, 4, and 5. Create a HashSet<int> from the existing IDs. Filter the incoming list to retain only IDs not in the set. Print each new person's ID.

Take Quiz

Keep learning

Related questions

View more