Entity Framework Core Pitfalls - Getting a Single Record by Key
Introduction
One more for the EF Core pitfalls series. EF Core offers essentially two ways to get a record with a known primary key:
- The native Find/FindAsync methods from DbContext or DbSet<T>
- A regular Where clause followed by one of First/FirstOrDefault or Single/SingleOrDefault, both from standard LINQ extensions, or FirstAsync/FirstOrDefaultAsync, SingleAsync/SingleOrDefaultAsync, the asynchronous alternatives from the EF Core LINQ extensions
There are a few differences between these approaches, and also a couple limitations, which I'll cover here. I will use as example a blogging domain model, where a Blog has many Posts, each Post references its Blog, and a Post has many Comments.
Like in other tips, this may or may not be a pitfall, but there is a possibility that we are not counting on this.
Using Find/FindAsync Methods
Find/FindAsync methods are great, because they allow us to retrieve a single entity without needing to know what is the property (or properties) that map to the primary key. There is also an enormous advantage: if the said entity was already loaded from the current DbContext, meaning, if it exists in the first level/object cache, then it is returned immediately. In general, this is what we want, but there are two problems with this:
- If the entity became stale, meaning, if changes happened in the database after the entity was loaded, then we have data that does not match what is in the database. We need to reload it by calling Reload or ReloadAsync on the entity entry for the context (Entry):
BloggingContext ctx = ...;
var post = ctx.Posts.Find(1);
//data became stale, let's reload
ctx.Entry(post).Reload();
- Using Find/FindAsync does not allow eager loading of related data. We need to load it explicitly by calling Load/LoadAsync on the entity entry, for each reference or collection we want to load:
ctx.Entry(post).Reference(p => p.Blog).Load(); //for a single reference, one to one or many to one
ctx.Entry(post).Collection(p => p.Comments).Load(); //for multiple references, one to many or many to many
A possible alternative for this is auto includes: we can configure certain references (one to one, many to one) and collections (one to many, many to many) to be loaded automatically for a certain entity, either in OnModelCreating or in a configuration class (IEntityTypeConfiguration<T>):
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>().Navigation(p => p.Blog).AutoInclude();
}
This causes the referenced Blog to always be loaded together with a Post. If we want to skip it for a particular query, we can apply IgnoreAutoIncludes:
var post = ctx.Posts.IgnoreAutoIncludes().Find(1);
Using the Queryable Extension Methods
Using the built-in First/FirstOrDefault/Single/SingleOrDefault extension methods works if we know the primary key property (or properties), which we normally do. We can also apply eager loading by chaining Include and ThenInclude methods:
var postWithBlogAndComments = ctx.Posts
.Where(p => p.PostId == 1)
.Include(p => p.Blog)
.Include(p => p.Comments)
.SingleOrDefault();
We should be aware that if we include many references/collections, we may end up with a cartesian explosion of data, which essentially means more data will be sent from the database than needed, but this is another subject.
Usage of the EF Core asynchronous extension methods FirstAsync/FirstOrDefaultAsync/SingleAsync/SingleOrDefaultAsync is also a viable option, for when we want a fully asynchronous solution.
In both cases, we should follow these guidelines:
- Single/SingleAsync is for the case when we are absolutely certain that a record with the id exists, and is unique. If there are more than one, or if it doesn't exist, an exception will be thrown
- SingleOrDefault/SingleOrDefaultAsync is for when the record with that said id may not exist, but, if it does, it is unique
- First/FirstAsync is for when the condition will return at least one but possibly many entities, but we only care about the first one and in that case we should enforce ordering of the results - failing to do so when there may be many records is itself a pitfall
- FirstOrDefault/FirstOrDefaultAsync is for when the condition may or may not return at least one entity
In general, the performance of First/FirstAsync/FirstOrDefault/FirstOrDefaultAsync is better than that of Single/SingleAsync/SingleOrDefault/SingleOrDefaultAsync: the SingleXXX variants actually ask for 2 records, and not just one: this is how they enforce uniqueness on the results. But in terms of semantics, the FirstXXX variants seem to imply that more than one record may exist, which is wrong if we are querying by primary key, so SingleXXX seems more natural.
Of course, this approach disregards the first level cache, meaning, a query will always be issued, entities materialised, etc. One possible solution for this might be check the local cache first, and only if the entity is not there do we go to the database:
var postFromCache = ctx.Posts.Local.FindEntry(1)?.Entity; //will return null of the entity is not in the cache
Local contains all entities that have been previously loaded for the context and FindEntry searches by key.
Conclusion
As you can see, even for a simple operation, we have many choices. Choose wisely, as it may affect your performance or logic. Hope you find this tip useful!
Comments
Post a Comment