λ1012: Prefer the …OrNone method over …OrDefault with a null check
FirstOrDefault, LastOrDefault, SingleOrDefault and ElementAtOrDefault signal a missing element with null.
Funcky's FirstOrNone, LastOrNone, SingleOrNone and ElementAtOrNone return an Option<T> instead.
Cause
The result of one of the LINQ …OrDefault methods is immediately tested for null:
xs.FirstOrDefault() ?? fallbackxs.FirstOrDefault()?.Memberxs.FirstOrDefault() is null,is not null,== null,!= nullxs.FirstOrDefault() is { } x,is not { } x,is string s
The rule applies to sequences of a reference type. With nullable reference types enabled, sequences of a nullable
element type (IEnumerable<string?>) are left alone: there a null result is ambiguous between "no element" and
"the element is null". The overloads with an explicit default value (FirstOrDefault(fallback)) and the IQueryable
variants are not reported either.
Reason for rule
A null has to be checked by every caller, and nothing forces the check. FirstOrNone returns an Option<T>, so the
missing element is a None and the handling is a GetOrElse, Select, Where or Match on the option. The code
says what happens when there is no element, and the compiler says when that case is forgotten.
How to fix violations
Replace the …OrDefault call with its …OrNone counterpart and handle the option. For xs.FirstOrDefault() ?? fallback
the code fix produces xs.FirstOrNone().GetOrElse(fallback). A ?? evaluates its fallback only when the left side is
null, so a fallback that does work (a call, an object creation, an interpolated string, a throw) is passed as a
function, GetOrElse(() => CreateFallback()), and keeps that behaviour. There is no fix when the ?? widens the type
(values.FirstOrDefault() ?? new object() is an object, which Option<string>.GetOrElse cannot return) or when the
fallback cannot move into a lambda (an await, a ref parameter). The other shapes have no single idiomatic
replacement, so they are reported without a fix:
| Before | After |
|---|---|
xs.FirstOrDefault() ?? fallback | xs.FirstOrNone().GetOrElse(fallback) |
xs.FirstOrDefault()?.Name | xs.FirstOrNone().Select(x => x.Name) |
xs.FirstOrDefault() is null | !xs.Any() or xs.FirstOrNone().Match(none: true, some: False) |
if (xs.FirstOrDefault() is { } x) { A(x); } else { B(); } | xs.FirstOrNone().Switch(none: B, some: A) |
Examples
Disallowed
static string Example(IEnumerable<string> names)
=> names.FirstOrDefault() ?? "nobody";
static int Example(IEnumerable<string> names)
=> names.SingleOrDefault()?.Length ?? 0;
static void Example(IEnumerable<string> names)
{
if (names.LastOrDefault() is { } last)
{
Console.WriteLine(last);
}
}
Allowed
static string Example(IEnumerable<string> names)
=> names.FirstOrNone().GetOrElse("nobody");
static int Example(IEnumerable<string> names)
=> names.SingleOrNone().Select(name => name.Length).GetOrElse(0);
static void Example(IEnumerable<string> names)
=> names.LastOrNone().Switch(none: NoOperation, some: Console.WriteLine);
// A null element is a value here, not a missing element.
static string Example(IEnumerable<string?> names)
=> names.FirstOrDefault() ?? "nobody";