The Dangerous EF Core Feature: Automatic Client Evaluation
Update: starting from EF Core 3.0-preview 4, this damaging default behavior has been greatly limited, although not completely turned off.
Recently when going through our shiny new ASP.NET Core application’s logs, I spotted a few entries like this:
The LINQ expression ‘foo’ could not be translated and will be evaluated locally.
I dug around in the code and found the responsible queries. Some of them were quite complex with many joins and groupings, while some of the other ones were quite simple, like
You may have spotted the problem right away.
StringComparison.OrdinalIgnoreCase is a .NET concept. It doesn’t translate to SQL and EF Core can’t be blamed for that. In fact, if you run the same query in the classic Entity Framework, you’ll get a
NotSupportedException telling you it can’t convert your perdicate to a SQL expression and that’s a good thing, because it prompts you to review your query and if you really want to have a predicate in your query that only applies in the CLR world, you can decide if it makes sense in your case to do a
ToList() or similar at some point in your
IQueryable to pull down the results of your query from the database into memory, or you may decide that you don’t need that
StringComparison.OrdinalIgnoreCase after all, because your database collation is case-insensitive anyway.
The point is that, by default you are in control and can make explicit decisions based on your circumstances.
That’s unfortunately not the case in Entity Framework Core because of its concept of mixed client/server evaluation. What mixed evaluation effectively does is, if you have anything in an
IQueryable LINQ query that can’t be translated to SQL, it tries to magically and silently make it work for you, by taking the untranslatable bits out and running them locally… and it’s enabled by default! what could go wrong?
That’s an extremely dangerous behavior change compared to the good old Entity Framework. Consider this entity:
And this query:
EF Core can’t translate
p.LastName.Equals("Amini", StringComparison.OrdinalIgnoreCase) into a query that can be run on the database, so it pulls down the whole Persons table, as well as the whole Orders and Addresses tables from the database into the memory and then runs the
.Where(p => p.LastName.Equals("Amini", StringComparison.OrdinalIgnoreCase)) filter on the results 🤦🏻♂️
The fact that this behavior is enabled by default is mind blowing! It’s not hard to imagine the performance repercussions of that on any real-size application with significant usage. It can easily bring down applications to their knees. Frameworks should make it difficult to make mistakes, especially ones with potentially devastating consequences like this.
You might be thinking that it’s the developer’s fault for including something like
StringComparison.OrdinalIgnoreCase in the IQueryable prediate, but having untranslatable things like that in your query isn’t the only culprit that results in client evaluation. If you have too many joins or groupings, the query could become too complex for EF Core and make it fall back to local evaluation.
So if you’re using EF Core, you want to keep an eye on your logs to spot client evaluations. If you don’t want that additional cognitive overhead, you can disable it altogether and make it throw like the good old Entity Framework:
However, I found that disabling client evaluation made EF core so limited, that it became practically unusable. That’s why I’m staying away from it for now and will re-evaluate it when version 3 is out to see if it’s production-ready yet.
I'm a software consultant, tech lead, servant team lead, C♯ and .NET aficionado, occasional public speaker, gardener, camping enthusiast and certified PSM I based in Brisbane, AU. I'm passionate about making positive impacts through technology, good software craftsmanship practices and effective project management.
Being a consultant exposes me to a wide variety of challenges, both technical and non-technical, and results in diverse and invaluable learning. This blog's main purpose is to serve as a self-reminder of some of those learnings that I've found the time to write down, but if it happens to help you too, that's even better!