Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SingleOrDefault and FirstOrDefault returning cached data

Some previous code I had written used the Find() method to retrieve single entities by their primary key:

return myContext.Products.Find(id)

This worked great because I had this code tucked into a generic class, and each entity had a different field name as its primary key.

But I had to replace the code because I noticed that it was returning cached data, and I need it to return data from the database each call. Microsoft's documentation confirmed this is the behavior of Find().

So I changed my code to use SingleOrDefault or FirstOrDefault. I haven't found anything in documentation that states these methods return cached data.

Now I am executing these steps:

  1. Save an entity via EF.
  2. Execute an UPDATE statement in SSMS to update the recently saved record's Description field.
  3. Retrieve the entity into a new entity variable using SingleOrDefault or FirstOrDefault.

The entities being returned still have the old value in the Description field.

I have run a SQL trace, and verified that the data is being queried during step 3. This baffles me - if EF is making a round trip to the database, why is it returning cached data?

I've searched online, and most answers apply to the Find() method. Furthermore, they suggest some solutions that are merely workarounds (dispose the DbContext and instantiate a new one) or solutions that won't work for me (use the AsNoTracking() method).

How can I retrieve my entities from the database and bypass the EF cache?

like image 629
ChadSC Avatar asked Jul 16 '19 16:07

ChadSC


2 Answers

The behaviour you're seeing is described in Microsoft's How Queries Work article under point 3:

  1. For each item in the result set

a. If this is a tracking query, EF checks if the data represents an entity already in the change tracker for the context instance

  • If so, the existing entity is returned

It's described a little better in this blog post:

It turns out that Entity Framework uses the Identity Map pattern. This means that once an entity with a given key is loaded in the context’s cache, it is never loaded again for as long as that context exists. So when we hit the database a second time to get the customers, it retrieved the updated 851 record from the database, but because customer 851 was already loaded in the context, it ignored the newer record from the database (more details).

All of this is saying that if you make a query, it checks the primary key first to see if it already has it in the cache. If so, it uses what's in the cache.

How do you avoid it? The first is to make sure you're not keeping your DbContext object alive too long. DbContext objects are only designed to be used for one unit of work. Bad things happen if you keep it around too long, like excessive memory consumption.

  • Do you need to retrieve data to display to the user? Create a DbContext to get the data and discard that DbContext.
  • Do you need to update a record? Create a new DbContext, update the record and discard that DbContext.

This is why, when you use EF Core with dependency injection in ASP.NET Core, it is created with a scoped lifetime, so any DbContext object only lives for the life of one HTTP request.

In the rare case you really do need to get fresh data for a record you already have an object for, you can use EntityEntry.Reload()/EntityEntry.ReloadAsync like this:

myContext.Entry(myProduct).Reload();

That doesn't help you if you only know the ID though.

If you really really need to reload an entity that you only have the ID for, you could do something weird like this:

private Product GetProductById(int id) {
    //check if it's in the cache already
    var cachedEntity = myContext.ChangeTracker.Entries<Product>()
                           .FirstOrDefault(p => p.Entity.Id == id);
    if (cachedEntity == null) {
        //not in cache - get it from the database
        return myContext.Products.Find(id);
    } else {
        //we already have it - reload it
        cachedEntity.Reload();
        return cachedEntity.Entity;
    }
}

But again, this should only be used in limited cases, when you've already addressed any cases of long-living DbContext object because unwanted caching isn't the only consequence.

like image 154
Gabriel Luci Avatar answered Oct 22 '22 17:10

Gabriel Luci


Ok, I have the same problem and finally found the answer,
You doing everything right, that's just how EF works. You can use .AsNoTracking() for your purposes:

return myContext.Products.AsNoTracking().Find(id)

make sure you addedusing Microsoft.EntityFrameworkCore; at the top.

It works like a magic

like image 1
Peyman Majidi Avatar answered Oct 22 '22 16:10

Peyman Majidi