We have a bit of code to reserve an account from an available pool, which looks like this:
var account = accounts.OrderBy(x => x.UsageCount).FirstOrDefault(x => x.TryReserve(token));
After porting our code from .NET Framework to .NET Core, this now invokes the predicate method for every item in the list. In practise, this code now reserves ALL accounts and then returns the first.
The problem is with the OrderedEnumerable.TryGetFirst method, which is invoked from the FirstOrDefault extension (presumably as a performance optimization). It orders items lazily and skips elements that do not match the predicate, which therefore has the side-effect of invoking the predicate once per element.
Given how ubiquitous .OrderBy(...).FirstOrDefault(...) is in code bases everywhere, I am surprised that this change in behavior was acceptable. While it's true that you should be careful with predicates with side effects, coalescing the two operations into one significantly changes what the code does (it is not ordering followed by filtering, as the code reads, but instead an amalgam of the two). Additionally, it's subtle and may go undetected because the code compiles just fine - the worst kind of a breaking change.
@stephentoub
We have a bit of code to reserve an account from an available pool, which looks like this:
var account = accounts.OrderBy(x => x.UsageCount).FirstOrDefault(x => x.TryReserve(token));After porting our code from .NET Framework to .NET Core, this now invokes the predicate method for every item in the list. In practise, this code now reserves ALL accounts and then returns the first.
The problem is with the
OrderedEnumerable.TryGetFirstmethod, which is invoked from theFirstOrDefaultextension (presumably as a performance optimization). It orders items lazily and skips elements that do not match the predicate, which therefore has the side-effect of invoking the predicate once per element.Given how ubiquitous
.OrderBy(...).FirstOrDefault(...)is in code bases everywhere, I am surprised that this change in behavior was acceptable. While it's true that you should be careful with predicates with side effects, coalescing the two operations into one significantly changes what the code does (it is not ordering followed by filtering, as the code reads, but instead an amalgam of the two). Additionally, it's subtle and may go undetected because the code compiles just fine - the worst kind of a breaking change.@stephentoub