2

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 and ChangeTracker.DebugView shows the IsReady property is Modified and the value was false and is now true, as expected
  • My code above sets UpdatedTime
  • SaveChangesAync completes and SQL logging shows that only the IsReady column is updated, but NOT UpdatedTime 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?

1 Answers1

0

I found this issue on github about this: https://github.com/dotnet/EntityFramework.Docs/issues/3888

The proposed workaround is setting the new value like this:

e.Entry.Property(nameof(Entity.ChangedAt)).CurrentValue = DateTime.Now;

It worked for me.

Andras Toth
  • 576
  • 4
  • 11