Single Elements and Defaults
First, Single and ElementAt pull one element out of a sequence; the OrDefault twins return a default instead of throwing.
TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)Sometimes a query should produce one element, not a sequence. LINQ has four pairs for that, and choosing between them is a statement about what you believe the data looks like.
Which throws, which does not
First() returns the first element, or the first that passes a predicate. It throws InvalidOperationException when there is nothing to return. Last() does the same from the other end. ElementAt(index) is positional and zero-based, and throws ArgumentOutOfRangeException when the index is past the end. Since .NET 6 ElementAt also accepts an Index, so ElementAt(^1) means the last element.
Each has an OrDefault twin: FirstOrDefault, LastOrDefault, ElementAtOrDefault. These return default(T) instead of throwing, which is null for a class and 0 for an int. With nullable reference types enabled they are typed T?, so the compiler makes you handle the null.
Single is a different question
Single does not mean "the first one". It means "there is exactly one", and it throws InvalidOperationException both when there are none and when there are two or more. That makes it the right operator for a lookup by a key you believe is unique: if the data ever holds two matches, you want a loud failure rather than an arbitrary row. SingleOrDefault keeps the uniqueness check but treats "no match" as a normal answer.
First(p) is shorthand for Where(p).First(); the same holds for the other three pairs. Since .NET 6 FirstOrDefault, LastOrDefault and SingleOrDefault also accept a default value of your choosing as the last argument; ElementAtOrDefault has no such overload.
Example
string[] users = ["ada", "bea", "cal"];
// First takes the first element, or the first that passes a test. It throws
// InvalidOperationException when there is nothing to return.
Console.WriteLine(users.First());
Console.WriteLine(users.First(u => u.StartsWith('b')));
// Last does the same from the other end.
Console.WriteLine(users.Last());
// FirstOrDefault returns default(T) rather than throwing. For string that
// default is null, so with nullable reference types on, the result is
// string? and the compiler insists you deal with the null.
string? absent = users.FirstOrDefault(u => u.StartsWith('z'));
Console.WriteLine(absent is null);
// Since .NET 6 you can supply the default yourself as the last argument.
Console.WriteLine(users.FirstOrDefault(u => u.StartsWith('z'), "nobody"));
// Single means "exactly one". Zero matches and two matches both throw
// InvalidOperationException, which is the whole point of using it.
Console.WriteLine(users.Single(u => u[0] == 'c'));
// ElementAt is positional and zero-based. ElementAtOrDefault is the form
// that returns default(T) instead of throwing on a bad index.
Console.WriteLine(users.ElementAt(1));
Console.WriteLine(users.ElementAtOrDefault(9) is null);
// Output:
// ada
// bea
// cal
// True
// nobody
// cal
// bea
// TrueWhen to use it
- Load the one account matching a user id, and fail loudly if the database holds two.
- Return 404 from an API handler when no record matches the requested id.
- Read the first line of a parsed file that looks like a header, without crashing on an empty file.
More examples
Why OrDefault is a trap for value types
This is the bug that gets filed as "the search returns the wrong row". It bites any sequence of int, bool, DateTime or an enum, because every one of those has a default that could also be a real value. The .NET 6 default-value overload is the cleanest fix when a sentinel exists; Any is the honest fix when it does not.
int[] offsets = [-3, 0, 5];
// default(int) is 0. A query that found nothing and a query that found a
// genuine zero both hand back 0, and nothing in the result tells them
// apart. For a string this is less dangerous, because null is not a value
// that would normally be sitting in the data.
int notFound = offsets.FirstOrDefault(n => n > 100);
int realZero = offsets.FirstOrDefault(n => n == 0);
Console.WriteLine($"{notFound} {realZero}");
// Two ways out. Pass a default that cannot occur in the data, or ask the
// existence question separately with Any.
int sentinel = offsets.FirstOrDefault(n => n > 100, int.MinValue);
bool exists = offsets.Any(n => n > 100);
Console.WriteLine($"{sentinel} {exists}");
// Output:
// 0 0
// -2147483648 FalsePicking Single, SingleOrDefault or First on purpose
The choice documents an assumption, so make it deliberately rather than reaching for First everywhere. One practical difference worth knowing: when these run against a database through EF Core, First sends a query that asks for one row, while Single asks for two so it can detect a duplicate. Using Single on a column with no unique constraint turns a silent data problem into a visible one.
Account[] accounts =
[
new Account(7, "ada"),
new Account(9, "bea")
];
// Single for a lookup by a key you believe is unique. If the data ever
// holds two rows with id 7, a thrown exception is the correct outcome.
Account found = accounts.Single(a => a.Id == 7);
// SingleOrDefault keeps the uniqueness check but allows "no match" as a
// normal answer, which is what an endpoint returning 404 wants.
Account? maybe = accounts.SingleOrDefault(a => a.Id == 99);
// First for "give me one of these", where several matches are expected and
// any of them will do.
Account shortName = accounts.First(a => a.Name.Length == 3);
Console.WriteLine($"{found.Name} {maybe is null} {shortName.Name}");
// Output:
// ada True ada
record Account(int Id, string Name);
Discussion