The author of this GitHub issue reports unexpected behavior from FirstOrDefault when used after OrderBy. The predicate is called on every element, which is not the expected behavior. In the example, the predicate has a side effect, and calling it on all elements is undesirable.
C#
var account = accounts.OrderBy(x => x.UsageCount).FirstOrDefault(x => x.TryReserve(token));
// TryReserve is called for each items even if the first one returns true
LINQ provides a declarative way to work with collections. By declarative, I mean that you express what you want but not how it will be executed. Sometimes it may optimize multiple methods by combining them or by changing the execution order. It's very similar to SQL in that sense. The following example shows that execution can differ from what you might expect.
C#
var result = Enumerable.Range(0, 10)
.FirstOrDefault(x =>
{
Console.WriteLine(x);
return x == 3;
});
Console.WriteLine("Result: " + result);

result equals 3 as expected. The collection is enumerated until FirstOrDefault finds an item that matches the condition.
Let's add OrderBy before FirstOrDefault and let's see what happens:
C#
var result = Enumerable.Range(0, 10)
.OrderBy(x => x)
.FirstOrDefault(x =>
{
Console.WriteLine(x);
return x == 3;
});
Console.WriteLine("Result: " + result);

The result is still correct. However, the way it is computed may not be what you expect. Indeed, FirstOrDefault processes all items.
Let's split FirstOrDefault to Where and FirstOrDefault:
C#
var result = Enumerable.Range(0, 10)
.OrderBy(x => x)
.Where(x =>
{
Console.WriteLine(x);
return x == 3;
})
.FirstOrDefault();
Console.WriteLine("Result: " + result);

This time, the result is correct and execution proceeds as expected.
#Workaround
If you want to evaluate the predicate lazily (without any optimizations), you can write a custom extension method:
C#
public static class EnumerableExtensions
{
public static T LazyFirstOrDefault<T>(IEnumerable<T> items, Predicate<T> predicate)
{
foreach (var item in items)
{
if (predicate(item))
return item;
}
return default;
}
}
Another way is to use the AsActualEnumerable() extension method from Meziantou.Framework to prevent the optimization:
C#
// Requires <PackageReference Include="Meziantou.Framework" Version="2.17.0" />
var result = Enumerable.Range(0, 10)
.OrderBy(x => x)
.AsActualEnumerable() // Wrap the enumerable into a custom IEnumerable<T> instance, so no *optimization* applies
.FirstOrDefault(x =>
{
Console.WriteLine(x);
return x == 3;
});
You can also convert your code to use a foreach loop. Starting with VS 15.7, Roslyn provides a code fix to convert between LINQ and foreach, as described in this GitHub issue.
#Conclusion
When using LINQ, it is important to understand that it is a declarative language, not an imperative one. You will get the expected result, but you have no control over how it is computed. You cannot rely on when or how many times a lambda will be executed. For this reason, you should only use side-effect-free methods to avoid unexpected behavior.
Note that the documentation says nothing about the actual implementation or how predicates will be invoked.
This issue is just one of the potential risks of LINQ. Other optimizations may also change the way your code behaves, such as .Skip allows a Collection to be modified during enumeration.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!