This post is the continuation of the Entity Framework Core Internals series, if you haven’t followed along please check out the previous posts below:
- Entity Framework Core Internals: High-Level Overview
- Entity Framework Core Internals: DbContext Instantiation and Initialization
- Entity Framework Core Internals: Dependency Injection and Internal Service Provider
- Entity Framework Core Internals: Model Building
In today’s article, we’ll dive deep to see how EF Core processes LINQ queries, translates, and executes them on your target database. We’ll also explore some concepts about IQueryable and Query Providers, and see how EF Core uses caching to make it work effectively.
IEnumerable vs IQueryable
IEnumerable is an abstraction in .Net referring to something you can enumerate over.
Let’s take a closer look at the
Because we enumerate a list, let’s drill down to the
As you can see, when enumerating, the predicate we passed into is executed to evaluate and filter the data, in our example any Blog that has BlogId > 1 is returned.
IQueryable is something similar to IEnumerable, however, it’s not a stream of data that we can enumerate now, it’s basically an abstraction of something we can query over.
For example: we don’t want to get the whole data from the SQL Server database and apply the filter, we want to translate the instruction to SQL, and then send it to the server, the server evaluates the whole thing and sends back the results. So we can think of EF Core as a special kind of compiler.
Let’s take a closer look at the
Where method, this time it’s different:
This time the predicate is an
Expression<Func<TSource, bool>> and is not
Func<TSource, bool> so what’s the difference:
Expression<Func<TSource, bool>> is a
LambdaExpression that captures a block of code that is similar to a .NET method body. In our example: it’s not a method that receives a Blog and returns true if BlogId > 1, it represents a method that receives a Blog and returns true if BlogId > 1.
Let’s make our query a bit complicated:
And examine the
If we visualize the Expression Tree now, it should look like below:
Now that we have the Expression Tree, how does EF Core (or any other ORMs) process the Expression Tree, before investigating that, let’s talk about
The easy way to understand how
IQueryableProvider work is to create our own simple implementations of
First, create our
MyQueryable class to represent something that we can query over:
The class depends on
IQueryProvider Provider to process the Expression, let create
MyQueryProvider class next:
Rather than process the expression directly inside the
Execute method, let’s create our own
ExpressionVisitor class to traverse the expression and translate it to an SQL query.
Finally, let’s use the
MyQueryable class in our application:
Set a breakpoint and debug the application, you should see the SQL statement constructed and we can use it to send to the SQL server (of course the statement is not working yet, but it demonstrates how we can process the expression).
If you recall one of the posts in the series Entity Framework Core Internals: DbContext Instantiation and Initialization, we figured out EF Core has its own implementation of
And here is the
Execute method of the
EntityQueryProvider class (implementation of
IAsyncQueryProvider which also is an
It uses the
IQueryCompiler _queryCompiler dependency to invoke the
Execute method, and here is the code of the
Execute method in the
QueryCompiler class (default implementation of the
Now you have a basic understanding of how EF Core will translate and process our queries, in reality, it’s a bit more complex as EF Core supports us in writing so many kinds of complex queries, the implementation code is supper complicated so rather showing the code here, we should use some diagrams to visualize it, let’s get started.
Query Provider (
EntityQueryProvider class is an
IQueryProvider) as mentioned in the last section, is responsible for handling the expression and return results, but it doesn’t do much, it just forwards the expression to the Query Compiler.
Query Compiler (QueryCompiler class) is responsible for compiling the query tree, compiling the logic to invoke the Translator, and compiling the Materializer.
Translator is responsible for traversing the expression tree and generating the instruction (ex: SQL script) that can be understood by the underlying database.
Materializer is code generated at runtime to create optimized code for the user’s model. It’s responsible for reading back the database results and materializing them. This is also a part of the query compilation.
One thing EF does pretty well is it’s using cache to avoid the same expression getting compiled repeatedly.
As you can see, the cacheKey is based on the query expression. However, if we just use the query being passed to generate the cacheKey, there is one issue which is if there are multiple queries with the same structure but different parameters, multiple queries will get compiled and cached even if they are very much the same. For example:
To overcome this issue, EF Core extracts the parameters and generalizes the query.
Below is the query with id = 1 before generalizing:
And the query with id = 2 before generalizing:
As you can see, after generalizing both queries look the same now, let’s get back and update the diagram:
If you are working with Compiled Queries, the way it works is very the same, it takes your predefined query, extracts parameters, compiles the query, and returns a delegate (contains the logic to translate and materialize data), when you use that delegate to execute your query multiple times, it doesn’t need to go through the entire process again and again.
I also have a post related to Compiled Queries, in case you’re interested please check it out: Entity Framework Core Tips: Making Queries Run As Fast As Dapper
Now that we know the concepts about IQueryable, and Query Providers, and also understand how EF Core does all the hard work to process our LINQ queries effectively.
That should be the end of the Internals series, thanks for following up with me from the beginning, the diagrams in this article were drawn using draw.io, if you are interested please get the diagrams file here.