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
IsReady
property is updated in an object that was previously returned by a query and tracked, but no EF functions are called SaveChangesAsync
is called- My
StateChanged
event handler is called - Inside my event handler, I can see that the Entity is
Modified
andChangeTracker.DebugView
shows theIsReady
property isModified
and the value wasfalse
and is nowtrue
, as expected - My code above sets
UpdatedTime
SaveChangesAync
completes and SQL logging shows that only theIsReady
column is updated, but NOTUpdatedTime
as 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?