Entity Framework relies on the mechanism of tracking (see this and this). It is not a caching per se but it can have similar effects though one the main goals is to track the changes so they can be propagated to the database.
Change tracking is "tied" to the concrete instance of the context (i.e. is not shared across different instances/globally). One of the important capabilities of the change tracking is identity resolution:
Since a tracking query uses the change tracker, EF Core will do identity resolution in a tracking query. When materializing an entity, EF Core will return the same entity instance from the change tracker if it's already being tracked. If the result contains the same entity multiple times, you get back same instance for each occurrence. No-tracking queries don't use the change tracker and don't do identity resolution. So you get back a new instance of the entity even when the same entity is contained in the result multiple times.
Which can lead to stale data in case if the database was updated "outside" between the requests in the same context:
using (var ctx1 = new AppContext())
{
var entity = await ctx1.SomeEntity
.Where(e => e.Id == 1)
.FirstOrDefaultAsync();
using (var ctx2 = new AppContext())
{
var theSameEntity = await ctx2.SomeEntity
.Where(e => e.Id == 1)
.FirstOrDefaultAsync();
theSameEntity.SomeTextField = "Updated"; // assuming it had another value
await ctx2.SaveChangesAsync();
}
var entity1 = await ctx1.SomeEntity
.Where(e => e.Id == 1)
.FirstOrDefaultAsync()
var referenceEquals = object.ReferenceEquals(entity, entity1); // True
var field = entity1.SomeTextField; // field will have the original value, not "Updated"
}
The 2nd call is much faster which I believe is because EF has cached my query and metadata
There are multiple reasons for it. If the first query was the first time this query was executed in this app run then EF Core can cache the query translating results to be reused in subsequent queries (across the context instances). Also current iteration of EF will not recreate and remap fetched data if entity is already tracked. Not sure if I remember correctly but previous iterations of EF/ EF Core potentially could have completely skip querying the database at all if already tracked entity was requested, but I can be wrong here.
but it doesn't appear to be caching the actual results of the query.
It is partly true and partly false at the same moment. As shown before - EF will not update tracked entity if the data actually has changed. But if data was removed EF will catch that:
using (var ctx1 = new AppContext())
{
var entity = await ctx1.SomeEntity
.Where(e => e.Id == 1)
.FirstOrDefaultAsync();
using (var ctx2 = new AppContext())
{
var theSameEntity = await ctx2.SomeEntity
.Where(e => e.Id == 1)
.FirstOrDefaultAsync();
ctx2.DeliveryComment.Remove(theSameEntity); // remove
await ctx2.SaveChangesAsync();
}
var entity1 = await ctx1.SomeEntity
.Where(e => e.Id == 1)
.FirstOrDefaultAsync()
var isNull = entity1 == null; // True
}