I have an EFCore entity that contains an UpdatedTime property that I would like to always have set to DateTimeOffset.UtcNow when the entity's State changes to Modified.
Using sample code found in another question I created the following event handler:
static void OnEntityStateChanged(object? sender, EntityStateChangedEventArgs e)
{
if (e.Entry.Entity is not IHasUpdatedTime entityWithTimestamps)
{
return;
}
switch (e.Entry.State)
{
case EntityState.Modified:
entityWithTimestamps.UpdatedTime = DateTimeOffset.UtcNow;
break;
// Other cases elided
}
}
In most cases this works as expected.
However, if the case of a very simple entity where only a single bool property IsReady is changed, it does not work as expected.
The symptoms are:
- The
IsReadyproperty is updated in an object that was previously returned by a query and tracked, but no EF functions are called SaveChangesAsyncis called- My
StateChangedevent handler is called - Inside my event handler, I can see that the Entity is
ModifiedandChangeTracker.DebugViewshows theIsReadyproperty isModifiedand the value wasfalseand is nowtrue, as expected - My code above sets
UpdatedTime SaveChangesAynccompletes and SQL logging shows that only theIsReadycolumn is updated, but NOTUpdatedTimeas expected.
Looking at the differences in stack traces between this case and another that works, in the working case it appears that DetectChanges is getting called before SaveChangesAsync.
My theory is that when a StateChanged handler is called from within DetectChanges, and that handler changes another property, it is indeterminate on if DetectChanges will detect that change before it completes. If the newly changed property had already been "checked", the newly changed property would be missed and thus not updated in the database. Since in this case it is SaveChangesAsync that is calling DetectChanges, there is no other chance for it to be called again.
With some more debugging, I can see that ChangeTracker.DebugView shows the UpdatedTime Property as changed from the original, but it is not "Modified". Further debugging into the internals shows that Property.IsModified is false.
When I change the above code to be as follows:
case EntityState.Modified:
entityWithTimestamps.UpdatedTime = DateTimeOffset.UtcNow;
if (!e.Entry.Property("UpdatedTime").IsModified)
{
e.Entry.Property("UpdatedTime").IsModified = true;
}
break;
Now the IsReady property is reliably updated.
Is this analysis correct?
Is there a better way to handle this other than modifying internal state?
Is this a defect in Change Detection? Should the change of an unmodified property in a StateChanged handler be detected?