Mert Özen Further With Every Line
Backend LINQ — #11

LINQ Any, All, Contains, SequenceEqual and the Conversion Operators

Mert Özen Nov 7, 2026 10 min 2 views
LINQ Any, All, Contains, SequenceEqual and the Conversion Operators

Why All returns true on an empty sequence and why validation needs Any as well, how Any stops at the first element versus Count() > 0, Contains and record equality, SequenceEqual and the trap of records carrying collections, snapshots with ToList/ToArray/ToHashSet, duplicate keys in ToDictionary, and the difference between Cast and OfType.

Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.

Guess First

We're writing a check that validates the cart before the payment step: the quantity of every line must be greater than zero. While testing, the cart stayed empty. What does this code print?

List<CartItem> cart = new();

bool allSmall = cart.All(i => i.Quantity <= 5);
bool anyItem = cart.Any();
Console.WriteLine($"All: {allSmall}, Any: {anyItem}");

if (cart.All(i => i.Quantity > 0))
{
    Console.WriteLine("Cart is valid, proceeding to payment");
}

record CartItem(string Isbn, int Quantity);

Why All Returns True on an Empty Sequence

Output:

All: True, Any: False
Cart is valid, proceeding to payment

Any said false on the empty cart, as expected. All, however, returned true twice, and an empty cart was considered "valid" and sent to payment. This isn't a bug, it's the logic itself. All asks: "Is there an element that breaks the condition?" In an empty sequence there's no element that breaks the condition, so the answer is true. The documentation is explicit too: if the sequence is empty, All returns true. In logic this is called "vacuous truth"; the sentence "all the elephants in the room are pink" is true if there are no elephants in the room.

In real code this result is almost never what you want. To say "the cart is valid" you need two conditions: there's at least one item in the cart, and every item follows the rule. That is, cart.Any() && cart.All(i => i.Quantity > 0). This post covers LINQ's yes/no questions (Any, All, Contains, SequenceEqual) and the conversion operators that turn query results into concrete collections. The examples from here on use the series' bookstore data; put the file below next to Program.cs as Data.cs:

record Book(string Isbn, string Title, string Author, decimal Price, int Stock, int Year);

static class Store
{
    public static List<Book> Books { get; } = new()
    {
        new("978-0441172719", "Dune", "Frank Herbert", 12.99m, 4, 1965),
        new("978-0553293357", "Foundation", "Isaac Asimov", 9.99m, 0, 1951),
        new("978-0451524935", "1984", "George Orwell", 8.99m, 7, 1949),
        new("978-0261102217", "The Hobbit", "J.R.R. Tolkien", 10.99m, 2, 1937),
        new("978-0547928210", "The Fellowship of the Ring", "J.R.R. Tolkien", 15.99m, 3, 1954),
        new("978-0553382563", "I, Robot", "Isaac Asimov", 7.99m, 5, 1950),
        new("978-0060850524", "Brave New World", "Aldous Huxley", 11.49m, 1, 1932),
        new("978-0451526342", "Animal Farm", "George Orwell", 6.99m, 9, 1945),
        new("978-0441569595", "Neuromancer", "William Gibson", 10.49m, 0, 1984),
        new("978-0441478125", "The Left Hand of Darkness", "Ursula K. Le Guin", 9.49m, 6, 1969),
    };
}

Any and All: Stopping at the First Answer

All of these operators run immediately, but they don't have to read the whole source. Let's see how much gets read with a query that increments a counter on every element:

int reads = 0;
IEnumerable<Book> books = Store.Books.Where(b =>
{
    reads++;
    return true;
});

reads = 0;
bool any = books.Any();
Console.WriteLine($"Any(): {any}, reads: {reads}");

reads = 0;
bool countPositive = books.Count() > 0;
Console.WriteLine($"Count() > 0: {countPositive}, reads: {reads}");

reads = 0;
bool anyCheap = books.Any(b => b.Price < 9m);
Console.WriteLine($"Any(Price < 9): {anyCheap}, reads: {reads}");

reads = 0;
bool allAboveSeven = books.All(b => b.Price > 7m);
Console.WriteLine($"All(Price > 7): {allAboveSeven}, reads: {reads}");
Any(): True, reads: 1
Count() > 0: True, reads: 10
Any(Price < 9): True, reads: 3
All(Price > 7): False, reads: 8

Any() read a single element: finding one is enough for the answer. Count() > 0 answered the same question by counting ten elements. If the source were a List<T>, Count() would read the property directly and there'd be no difference; but on top of a query like Where, or a database query, that difference is a real cost. In EF Core, Any() is translated to EXISTS, while Count() becomes a COUNT that counts all the rows. The rule is simple: if you're asking "is there any?", write Any().

Any with a predicate stops at the first match, All at the first violation. The first book cheaper than 9 is third in the list (1984), the first book under 7 is eighth (Animal Farm). There's no difference between Where(...).Any() and Any(...) in result or in reads; prefer the shorter one.

Contains and Equality

string[] tags = { "SciFi", "Classic", "Bestseller" };
Console.WriteLine($"Contains(\"scifi\"): {tags.Contains("scifi")}");
Console.WriteLine($"OrdinalIgnoreCase: {tags.Contains("scifi", StringComparer.OrdinalIgnoreCase)}");

Book dune = new("978-0441172719", "Dune", "Frank Herbert", 12.99m, 4, 1965);
Console.WriteLine($"Same values: {Store.Books.Contains(dune)}");
Console.WriteLine($"Different stock: {Store.Books.Contains(dune with { Stock = 3 })}");
Console.WriteLine($"Same ISBN: {Store.Books.Any(b => b.Isbn == dune.Isbn)}");
Contains("scifi"): False
OrdinalIgnoreCase: True
Same values: True
Different stock: False
Same ISBN: True

Contains uses the same equality rules we saw in earlier posts. Since the default string comparison is case-sensitive, "scifi" wasn't found; the overload that takes a comparer found it. Because Book is a record, a new instance with all the same fields counted as "in" the list. A copy with a different stock wasn't found, because record equality looks at all the properties. Most of the time, when you say "the same book" you mean the ISBN matches; in that case Any(b => b.Isbn == dune.Isbn) states your intent more clearly.

On the performance side, Contains behaves according to the type of the source. If the source is an ICollection<T>, it calls that collection's own Contains: on a List<T> that's a scan from start to end (O(n)), on a HashSet<T> an average O(1) hash lookup. If you ask the same list Contains over and over inside a loop, turning the list into a HashSet once is the cheapest improvement.

SequenceEqual: Order Matters

string[] first = { "Dune", "1984", "The Hobbit" };
List<string> second = new() { "Dune", "1984", "The Hobbit" };
string[] reordered = { "1984", "Dune", "The Hobbit" };

Console.WriteLine($"==: {first == second.ToArray()}, SequenceEqual: {first.SequenceEqual(second)}");
Console.WriteLine($"Reordered: {first.SequenceEqual(reordered)}");
Console.WriteLine($"Sorted first: {first.OrderBy(t => t, StringComparer.Ordinal).SequenceEqual(reordered.OrderBy(t => t, StringComparer.Ordinal))}");
Console.WriteLine($"Ignore case: {first.SequenceEqual(new[] { "DUNE", "1984", "THE HOBBIT" }, StringComparer.OrdinalIgnoreCase)}");

Wishlist a = new("Ada", new List<string> { "Dune" });
Wishlist b = new("Ada", new List<string> { "Dune" });
Console.WriteLine($"record with a List: {a == b}, titles: {a.Titles.SequenceEqual(b.Titles)}");

record Wishlist(string Owner, List<string> Titles); // the List is compared by reference
==: False, SequenceEqual: True
Reordered: False
Sorted first: True
Ignore case: True
record with a List: False, titles: True

Comparing two arrays with == compares their references; even with the same contents the result is false. SequenceEqual compares the elements in order, pair by pair, and the lengths must match too. It can compare two collections of different types (an array and a List), but it cares about order: the same three books in a different order give false. If you don't care about order, sort both with the same rule first, or think in terms of set logic. There's an overload that takes a comparer as well.

The last line is a common trap when working with records. Wishlist is a record, but the List<string> inside it is a reference type in its own right. The equality the compiler generates compares that property with EqualityComparer<List<string>>.Default, that is, by reference. Even though the contents of the two wishlists are the same, a == b came out false. If you want equality for records that contain collections, you need to write Equals yourself and use SequenceEqual.

ToList, ToArray, ToHashSet: Freezing a Query

Conversion operators run a deferred query and copy its result into a new collection. From then on that collection is independent of the source:

List<Book> source = new(Store.Books);

IEnumerable<Book> inStock = source.Where(b => b.Stock > 0);
List<Book> list = inStock.ToList();
Book[] array = inStock.ToArray();
HashSet<string> authors = inStock.Select(b => b.Author).ToHashSet();

source.RemoveAll(b => b.Price > 10m); // a List method that changes the source, not LINQ

Console.WriteLine($"Query: {inStock.Count()}, list: {list.Count}, array: {array.Length}, authors: {authors.Count}");
Console.WriteLine($"Orwell in set: {authors.Contains("George Orwell")}");
Query: 4, list: 8, array: 8, authors: 6
Orwell in set: True

RemoveAll isn't a LINQ operator, it's a List<T> method that changes the source in place. After five books were removed from the source, the query ran again and found 4 books. The list and the array, however, hold the 8 books from the moment the query was called; the set holds the six authors from that moment. This behavior is useful in two ways: if you'll use a query's result more than once, you run it once, and you get a snapshot that isn't affected by later changes to the source.

Which one you pick depends on what you'll do with the result. If you'll add elements to the result or remove them, ToList(). If you want a result whose size won't change, ToArray(). If you'll frequently ask the result "is this in there?", ToHashSet(); it also drops duplicates. When returning data out of a method, one of these three is usually the right choice: the calling code shouldn't need to know that your query runs again every time it's enumerated.

ToDictionary and Duplicate Keys

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

Dictionary<string, Book> byIsbn = Store.Books.ToDictionary(b => b.Isbn);
Dictionary<string, decimal> priceByTitle = Store.Books.ToDictionary(b => b.Title, b => b.Price);
Console.WriteLine($"{byIsbn["978-0451524935"].Title}, Dune: {priceByTitle["Dune"]}");

try
{
    Dictionary<string, Book> byAuthor = Store.Books.ToDictionary(b => b.Author);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}

Dictionary<string, Book> firstByAuthor = Store.Books.DistinctBy(b => b.Author).ToDictionary(b => b.Author);
Dictionary<string, int> countByAuthor = Store.Books.GroupBy(b => b.Author).ToDictionary(g => g.Key, g => g.Count());
Dictionary<string, Book> ignoreCase = Store.Books.ToDictionary(b => b.Title, StringComparer.OrdinalIgnoreCase);

Console.WriteLine($"First Asimov: {firstByAuthor["Isaac Asimov"].Title}, Asimov count: {countByAuthor["Isaac Asimov"]}");
Console.WriteLine($"\"DUNE\" found: {ignoreCase.ContainsKey("DUNE")}");
1984, Dune: 12.99
An item with the same key has already been added. Key: J.R.R. Tolkien
First Asimov: Foundation, Asimov count: 2
"DUNE" found: True

ToDictionary takes a key selector, an optional element selector and an optional comparer. With a truly unique field like the ISBN it works without trouble. With a field that can repeat, like the author, it threw an ArgumentException at the second Tolkien book; the message even names the conflicting key. That's a good thing: if there's a duplicate in the data you didn't expect, you find out right away instead of silently losing one.

If duplicates are normal, you have three options. If the first element per key is enough, DistinctBy first. If you need a summary per key, GroupBy followed by ToDictionary(g => g.Key, ...). If you need all the elements of each key, go straight to ToLookup, as we saw in the grouping post. For keys that come from users (titles, usernames), don't forget the comparer either: the dictionary built with StringComparer.OrdinalIgnoreCase found the book for a "DUNE" lookup too.

Cast and OfType

LINQ operators work on IEnumerable<T>. Types left over from before generics, like ArrayList, only implement the non-generic IEnumerable. There are two ways to bring them into the LINQ world:

using System.Collections;

ArrayList legacy = new() { "Dune", 42, "1984", null, 3.5 };

Console.WriteLine($"OfType<string>: {string.Join(" | ", legacy.OfType<string>())}");

try
{
    foreach (string? title in legacy.Cast<string>())
    {
        Console.WriteLine($"Cast<string>: {title}");
    }
}
catch (InvalidCastException ex)
{
    Console.WriteLine(ex.Message);
}

int[] numbers = { 1, 2, 3 };
try
{
    List<long> longs = numbers.Cast<long>().ToList();
}
catch (InvalidCastException ex)
{
    Console.WriteLine(ex.Message);
}

List<long> converted = numbers.Select(n => (long)n).ToList();
Console.WriteLine($"Select: {string.Join(", ", converted)}");
Console.WriteLine($"Cast on List<Book> returns the same object: {ReferenceEquals(Store.Books, Store.Books.Cast<Book>())}");
OfType<string>: Dune | 1984
Cast<string>: Dune
Unable to cast object of type 'System.Int32' to type 'System.String'.
Unable to cast object of type 'System.Int32' to type 'System.Int64'.
Select: 1, 2, 3
Cast on List<Book> returns the same object: True

OfType<string>() is a filter: it lets through the elements whose type matches and skips the rest. 42, 3.5 and null were dropped. Cast<string>(), on the other hand, is a claim: "they're all strings". It yielded the first element and threw an InvalidCastException when it reached 42. Because it runs deferred, the error came up not when the query was defined but when that element was reached.

The second exception is a classic trap. Cast<long>() doesn't do a numeric conversion. It first boxes each element into an object, then tries to unbox it to long. Since there's an int in the box, the unboxing fails; the fact that there's an implicit conversion from int to long changes nothing. For a numeric conversion, write Select(n => (long)n). The last line shows an optimization too: if the source is already an IEnumerable<T> of the requested type, Cast produces nothing new and returns the same object. Since there's no .NET 6 runtime on this machine, the examples target net6.0 and ran on the .NET 8 runtime with DOTNET_ROLL_FORWARD=Major; the wording of exception messages can differ slightly between runtime versions.

Common Pitfalls

Validating a collection that may be empty with All alone. An empty sequence "passes" every rule. Write Any() && All(...).

Writing Count() > 0 to check for existence. On queries and in the database it counts everything for nothing; Any() stops at the first element.

Calling Contains on a large List inside a loop. Each call is an O(n) scan; call ToHashSet() first.

Comparing two collections with == or Equals. Those look at references; for contents, use SequenceEqual. For the same reason, equality of records that carry collections doesn't work the way you expect.

Building a ToDictionary on a field that can repeat. The day the data brings a duplicate, you get an ArgumentException; state your intent explicitly with ToLookup, GroupBy or DistinctBy.

Mistaking Cast<T>() for a type conversion. It only does reference and boxing conversions; it won't go from int to long. To filter a mixed collection use OfType<T>(), for numeric conversion use Select.

A Small Experiment

★☆☆ Query, with three different operators, whether the book list contains a book with a stock of 0, whether all the books were published after 1950, and whether "Neuromancer" is in the list.

★★☆ Write a function that checks whether two order lists contain the same ISBNs, regardless of order and duplicates. Solve it first with SequenceEqual, then with HashSet<T>.SetEquals, and explain the difference.

★★★ Rewrite the equality of the Wishlist record so that it works on the contents of Titles: define Equals(Wishlist? other) yourself and make sure GetHashCode is consistent with it. Test it with Distinct.

With this post we've seen most of LINQ's ready-made operators. In the next post we move to the other side of the table: we'll write our own LINQ operator with an extension method and yield return, and see how deferred execution works inside, in our own code.