Updated on

The LINQ Except() method finds the set difference between two sequences. It returns a new sequence with the elements of the first sequence that are not in the second one, and it returns each of those elements only once.

It comes in handy when we compare collections in C# or track what changed between them. Elements match through the default equality comparer, through an IEqualityComparer<T> we pass in, or, from .NET 6 on, through a key we give to ExceptBy().

To download the source code for this article, you can visit our GitHub repository.

How Does LINQ Except Work in C#?

Except() takes two sequences and returns the elements of the first one that do not appear in the second. It never returns elements that exist only in the second sequence, so a.Except(b) and b.Except(a) usually give different results.

The result is a set. If the first sequence holds the same value twice, it comes back once.

To decide whether two elements match, Except() uses the default equality comparer of the element type. Strings and numbers compare by value. A class compares by reference unless it overrides Equals() and GetHashCode(), so two Employee objects with identical properties still count as different.

Except() uses deferred execution. Calling it only builds the query, and the work happens when we enumerate the result, for example with ToList() or foreach. A null sequence is the one thing it checks right away: it throws an ArgumentNullException at the call.

We are going to use a complex type, such as an Employee class with ID, Name, Age and Department properties, for our examples:

List<Employee> employees = new() 
{
    new Employee {ID = 1, Name = "John Doe", Age = 28, Department = "Sales"},
    new Employee {ID = 2, Name = "Emily Sanders", Age = 29, Department = "Sales"},
    new Employee {ID = 3, Name = "Benjamin Winters", Age = 30, Department = "Sales"},
    new Employee {ID = 4, Name = "Sheila Foxx", Age = 31, Department = "Marketing"},
    new Employee {ID = 5, Name = "Alexander Flemming", Age = 31, Department = "Marketing"},
    new Employee {ID = 6, Name = "Grace Jones", Age = 32, Department = "IT"},
    new Employee {ID = 7, Name = "Ava Knowles", Age = 32, Department = "IT"},
    new Employee {ID = 8, Name = "Isabela Wambui", Age = 48, Department = "Marketing"},
    new Employee {ID = 9, Name = "David Otieno", Age = 60, Department = "HR"},
    new Employee {ID = 10, Name = "Emma Charlotte", Age = 33, Department = "HR"},
    new Employee {ID = 11, Name = "Michael Bay", Age = 34, Department = "HR"},
    new Employee {ID = 12, Name = "Mary Jane", Age = 35, Department = "Sales"}
};

Our examples live in one class: employees and the IT list we add later are its fields, and each snippet that ends in return is the body of one of its methods. Let’s assume we want to get a list of employees not in sales using Except():

var empNotInSales = employees.Except(employees.Where(e => e.Department.Equals("Sales"))).ToList();

return empNotInSales;

First, we find the set difference by returning elements from employees that are not in the sales department, which helps us achieve our goal. Note that when one of the sequences we are comparing is null, a System.ArgumentNullException will be thrown. 

In this example, we used the simplest overload, Except<TSource>(IEnumerable<TSource>, IEnumerable<TSource>), which compares elements with the default equality comparer.

The example works because Where() hands Except() the very same Employee objects that live in employees, so reference equality is enough. If we build the sales list from new objects with the same values, nothing gets removed:

List<Employee> salesCopies = new()
{
    new Employee {ID = 1, Name = "John Doe", Age = 28, Department = "Sales"},
    new Employee {ID = 2, Name = "Emily Sanders", Age = 29, Department = "Sales"},
    new Employee {ID = 3, Name = "Benjamin Winters", Age = 30, Department = "Sales"},
    new Employee {ID = 12, Name = "Mary Jane", Age = 35, Department = "Sales"}
};

return employees.Except(salesCopies).ToList();

All 12 employees come back, because none of the copies is the same object as an employee in our list. The next two sections show how to make Except() compare employees by their values.

How to Use an IEqualityComparer With the LINQ Except Method in C#

Sometimes, we may want to perform a set difference between two string sequences with different casing.

Let’s assume we have a list of employees in the IT department with employee names in lowercase:

List<Employee> employeesIT = new()
{
    new Employee {ID = 6, Name = "grace jones", Age = 32, Department = "IT"},
    new Employee {ID = 7, Name = "ava knowles", Age = 32, Department = "IT"}
};

The default comparator object is case sensitive; therefore, when we want to retrieve a list of employees not in the IT department, Ava and Grace will still be returned if we use it. 

Let’s implement an example with our list of employees to perform the set difference of their names while ignoring their case sensitivity:

var employeesNotInIT = employees.Select(e => e.Name)
     .Except(employeesIT.Select(s => s.Name), StringComparer.OrdinalIgnoreCase).ToList();

return employeesNotInIT;

Here, we pass StringComparer.OrdinalIgnoreCase as an argument to return the list of employees not in the IT department, regardless of their case. StringComparer already implements the IEqualityComparer interface, which helps us control how we compare string values. 

Ava Knowles and Grace Jones are gone from the result, even though the IT list spells their names in lowercase:

John Doe, Emily Sanders, Benjamin Winters, Sheila Foxx, Alexander Flemming, Isabela Wambui, David Otieno, Emma Charlotte, Michael Bay, Mary Jane

Next, let’s learn how to implement the IEqualityComparer interface using our Employee class example. 

Custom IEqualityComparer Implementation

In some cases, we may wish to have more granular control over performing set differences between two sequences, especially for complex types. Let’s implement the interface using our Employee class example to understand how the interface works:

public class EmployeeComparer : IEqualityComparer<Employee>
{
    public bool Equals(Employee? x, Employee? y)
    {
        if (x is null && y is null) return true;
        if (x is null || y is null) return false;

        return x.ID == y.ID && x.Name == y.Name && x.Age == y.Age && x.Department == y.Department;
    }

    public int GetHashCode(Employee obj) => HashCode.Combine(obj.ID, obj.Name);
}

The Equals() method checks whether two Employee objects are equal. First, if both objects are null, we treat them as equal and return true. Next, we check if any of the objects have null object references and return false. Finally, we compare each of the properties in our Employee class for equality. 

On the other hand, the GetHashCode() method combines the ID and Name properties into one hash code with HashCode.Combine().

Except() calls this method as well. It loads the second sequence into a hash set and calls Equals() only for elements whose hash codes match. So two employees that Equals() treats as equal must also return the same hash code. If they do not, Except() never compares them and keeps the element, the same rule that applies to keys in hash-based collections such as dictionaries.

We can now achieve the same objective as our previous example:

EmployeeComparer comparer = new ();

var employeesInSales = employees.Where(e => e.Department.Equals("Sales")).ToList();

return employees.Except(employeesInSales, comparer).ToList();

This time, when we call the Except() method, our custom implementation of the Equals() method will be used.

If we can change the type itself, a record saves us the comparer. The compiler gives a record value-based Equals() and GetHashCode() methods, so Except() matches records by their property values with no extra code:

public record EmployeeRecord(int ID, string Name, int Age, string Department);

Let’s run the same sales filter with records built from separate objects:

List<EmployeeRecord> staff = new()
{
    new EmployeeRecord(1, "John Doe", 28, "Sales"),
    new EmployeeRecord(4, "Sheila Foxx", 31, "Marketing"),
    new EmployeeRecord(6, "Grace Jones", 32, "IT")
};

List<EmployeeRecord> sales = new()
{
    new EmployeeRecord(1, "John Doe", 28, "Sales")
};

return staff.Except(sales).ToList();

John Doe is removed even though the two lists hold different objects, and the names that remain are:

Sheila Foxx, Grace Jones

How Does ExceptBy Compare Elements by Key?

ExceptBy() arrived in .NET 6. It compares elements by a key instead of by the whole element. We pass it the keys to exclude and a key selector for the first sequence, and it returns the elements whose key is not among those keys.

The second argument is a sequence of keys, not of elements. To drop the IT employees by ID, we select their IDs and let the selector read ID from each employee. We need no comparer and no record, and the casing problem from the IT list goes away because names are never compared.

The rules of Except() still apply, only now to the keys. The result holds each key once, so when two elements in the first sequence share a key, only the first of them comes back. Execution is deferred in the same way.

One more overload takes an IEqualityComparer<TKey>, which helps when the key is a string that should match regardless of case.

Here is the IT filter again, this time by ID:

return employees.ExceptBy(employeesIT.Select(e => e.ID), e => e.ID).ToList();

The result is the same ten employees the case-insensitive comparer gave us:

John Doe, Emily Sanders, Benjamin Winters, Sheila Foxx, Alexander Flemming, Isabela Wambui, David Otieno, Emma Charlotte, Michael Bay, Mary Jane

ExceptBy() shipped together with DistinctBy(), IntersectBy() and UnionBy(), and our overview of the LINQ improvements in .NET 6 walks through all four.

When Should We Use the LINQ Except Method?

We can use Except() when we want to retrieve unique sequences of elements, as it returns a new sequence after performing the set difference between two sequences. 

The duplicates rule is easy to miss with real data, so here it is on two small arrays:

int[] first = [1, 1, 2, 3, 3, 4];
int[] second = [3];

return first.Except(second).ToList();

As Microsoft Learn’s remarks for the method put it, “Only unique elements are returned.” Both 3s are gone, and the two 1s come back as one:

1, 2, 4

When we need every matching element, duplicates included, Except() is the wrong tool. Filtering the first list with Where() against the second keeps them, and we cover that approach in filtering a list based on another list. If removing duplicates is the whole goal, there are more direct options, which we cover in removing duplicates from a list.

Beyond that, we can use Except() when we want to track changes between two sequences. For example, when we add a new Employee object, we can track changes between two lists, forming the basis for tasks such as incremental backups and audit trails. The same idea gives us excluding joins in LINQ, where we keep the rows on one side that have no match on the other.

Finally, note that Except() does not make any changes to the original sequences but returns a new instance containing distinct elements. In practice, the results come out in the order of the first sequence, although the documentation does not promise it.

Conclusion

In this article, we learned how to use the Except method in LINQ. We can leverage this method to find the set difference of both simple and complex types. When we need greater control over comparing elements, we can leverage the IEqualityComparer interface, a record, or a key passed to ExceptBy().

Tested with .NET 10.0.10 and MSTest 4.5.2.