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.
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 inpeopleList2.existingIds.Contains(person.ID)istruewhen that ID is in the first list.!means “not”, so only people whose IDs are absent are retained.ToList()executes the query and creates aList<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();
peopleList1.Select(person => person.ID)produces the values1,2, and3.ToHashSet()creates a set containing{ 1, 2, 3 }.- LINQ checks each person in
peopleList2:- Person
1: the set contains1, so!trueisfalse; exclude it. - Person
2: the set contains2; exclude it. - Person
3: the set contains3; exclude it. - Person
4: the set does not contain4, so!falseistrue; include it. - Person
5: the set does not contain5; include it.
- Person
ToList()materializes the filtered sequence.- The final list contains IDs
4and5.
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.
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
| Approach | Example | Best use | Notes |
|---|---|---|---|
Where + HashSet.Contains | peopleList2.Where(p => !ids.Contains(p.ID)) | In-memory lists, especially large ones | Clear and typically efficient |
Where + Any | peopleList2.Where(p => !peopleList1.Any(x => x.ID == p.ID)) | Small lists and simple one-off code | Can repeatedly scan the first list |
Except on IDs | peopleList2.Select(p => p.ID).Except(peopleList1.Select(p => p.ID)) | When only the missing IDs are needed | Returns IDs, not Person objects |
Except with comparer | peopleList2.Except(peopleList1, comparer) | Reusable object comparison rules | Requires an IEqualityComparer<Person> |
Join / GroupJoin | Match items by a key across collections | When results need data from both sides | More 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.
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”.Wherereturns matching original objects.Selecttransforms 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
Exceptdirectly 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.
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.
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.