Avoid AsNoTracking and Include when querying using Entity Framework in ASP.NET
Developers often use AsNoTracking in the belief that it will increase performance when performing read queries in Entity Framework. This post explains why this approach is flawed and its usage may actually be a sign of an underlying problem - a problem that is made even worse by the use of Include.
HTTP is stateless, so in ASP.NET, when a page is displayed, we read data from the database and any subsequent saving of data is a completely separate request. Therefore, in most situations, when you query the initial data, you do not want Entity Framework to track it, as the DbContext will be destroyed at the end of the initial request.
If you submit a form, on postback, a new request will be issued, resulting in a new DbContext. At this stage (on update), we do want to track the data because this is how EF is able to work out what SQL needs to be executed in order to persist our changes.
New Request -> New DbContext -> Query Data -> DbContext Destroyed
New Request -> New DbContext -> Query Data (with tracking) -> Update Properties -> Save To Database -> DbContext Destroyed
It is quite clear to see that querying and updating are quite different situations. Querying doesn't require our data to be tracked and to do so is inefficient and may slow down the request. It seems as though AsNoTracking is exactly what we need!
The problem of course is that we should not be querying entities in the first place.
Given the following query:
var blogPosts = _db.BlogPosts.Where(b => b.IsPublished).ToList();
If we add AddNoTracking, then depending on the number of records, we should see an immediate performance improvement:
var blogPosts = _db.BlogPosts.AsNoTracking().Where(b => b.IsPublished).ToList();
But we have to ask ourselves, why are we retrieving a list of BlogPost entities in the first place?
In any professional code base, we do not want to pass entity framework entities directly to our UI or client. Whether we are using ASP.NET MVC, Razor Pages, Web API or Blazor, we know this is bad practice. We should use an intermediate class - a DTO or view model. The benefits of such an approach are (almost) universally accepted:
- Efficiency - we return the data that is actually needed, not every column in the table (or tables if we use Include.)
- Separation of Concerns - reduced coupling means that we can maintain a contract (e.g. a web api endpoint) even when the internal representation changes.
- Security - we can choose exactly what to expose and avoid overposting vulnerabilities without resorting to brittle hacks such as [Exclude].
- Ease of Use - we can merge and flatten data from multiple sources into a form that is most suitable for the client.
It is unfortunate that there are so many online code samples that omit view models in the name of brevity. AsNoTracking is used to increase performance, but returning entities instead of view models can affect performance far more. When we are doing things the right way, AsNoTracking becomes redundant.
Let's look at an example, focusing on performance, because after all, this is what AsNoTracking is supposed to help with.
Imagine a blogging application where you want to display a list of links to blog posts:
With AsNoTracking, you might write:
var blogPosts = _db.BlogPosts.AsNoTracking().ToList();
Ignoring the lack of pagination, this looks fairly innocuous, so what's the problem?
The problem is that we do not place any restrictions on which columns to return. It is quite likely that the BlogPost database table includes the actual text of the blog post. This could be many thousands of words for each blog post and yet we query it unnecessarily.
Of course, in many cases, we need data from multiple tables. What if we wanted to display the author and number of comments too?
Is this a use case for EF's Include?
var blogPosts = _db.BlogPosts.AsNoTracking().Include(b => b.Author).Include(b => b.Comments).ToList();
This does give us the data that we need but at what cost?
We are querying all blog post data, all author data for those blog posts and all comments for all blog posts! This is extremely inefficient, but also extremely common. Yes, AsNoTracking may help performance in this case, but it is a band-aid. It is hiding a bigger problem. The way that we are querying our data is fundamentally flawed.
The solution is to use a view model with a projection. In the first example, we only query the three fields we need:
var blogPosts = _db.BlogPosts.Select(b => new BlogPostModel(b.Title, b.Url, b.DatePublished)).ToList();
Note that because we use a projection, EF does not track any entity and the use of AsNoTracking is not necessary.
In the second example, we use navigation properties to query the author name field and count the number of comments. Our query is highly efficient and again AsNoTracking is not necessary:
var blogPosts = _db.BlogPosts.Select(b => new BlogPostModel(b.Title, b.Url, b.DatePublished, b.Author.Name, b.Comments.Count())).ToList();
The use of AsNoTracking and Include when simply returning data to the client is generally a sign that something is wrong. Either you are not using view models or you are querying entities and then mapping to view models in memory. In both cases, this is rarely a good idea and changing your approach will almost certainly result in performance gains in addition to the other benefits listed above.
When to use AsNoTracking
If you are using projections to map to view models (and you should be) then there is no need to ever use AsNoTracking.
When to use Include
As with AsNoTracking, if you are using projections to map to view models then Include should never be used when querying.
Includes does have an important use when updating however.
If you are updating a 'parent' entity (an aggregate root) such as an Order then you may well want to update child collections or other related data. In this scenario, Include can be used to query and track all required data in order to update it.
var order = _db.Orders.Include(o => o.OrderItems).First(o => o.Id = id); // update order entity and child collection of order items _db.SaveChanges();
Some of you might argue that view models are unnecessary in the most basic of CRUD applications but I do not agree with this and would recommend everyone use them in all cases. The benefits outweigh the very small overhead required, and with C# records, it is easier than ever.
Useful or Interesting?
If you liked the article, I would really appreciate it if you could share it with your Twitter followers.Share on Twitter