2

I implemented similar solution on how we can modify created and updated date upon saving data through EF Core as what is suggested here Populate Created and LastModified automagically in EF Core.

void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
    if (e.NewState == EntityState.Modified && e.Entry.Entity is IHasCreationLastModified entity)
        entity.LastModified = DateTime.Now;
}

At first I thought this will be triggered only when SaveChanges() is called. But apparently it is also called on Entry()

// Get entity
var student = _dbContext.Students.Find(studentId);

// Modify student object
student.Name = "New student name";

// Called Entry(), trigger ChangeTracker.StateChanged
var entry = _dbContext.Entry(student);

// Doesn't trigger ChangeTracker.StateChanged
_dbContext.SaveChanges();

I found that ChangeTracker.StateChanged is triggered when _dbContext.Entry(student) is called. Then it doesn't get triggered again when _dbContext.SaveChanges() is called. And it also passes the condition above if (e.NewState == EntityState.Modified && e.Entry.Entity is IHasCreationLastModified entity).

My assumption why it is not triggered again when SaveChanges() is called, because there is no new update to the entity after Entity() is called.

This results in the LastModified property being assigned when .Entry(student) is called, instead of when .SaveChanges() is called.

Is there a way to only update LastModified property once when SaveChanges is called on the scenario above?

muhihsan
  • 2,250
  • 6
  • 27
  • 42
  • override `public virtual int SaveChanges(bool acceptAllChangesOnSuccess)` - there you can access `ChangeTracker`, found all entities with `Modified` state and update their `LastModified` property right before writing to db. – vasily.sib Mar 20 '19 at 02:48

2 Answers2

3

I suggest that you could override you SaveChanges method in your dbContext. You could refer to below code that I usually use.

public class ForumContext : DbContext
{
    public ForumContext(DbContextOptions<ForumContext> options) : base(options)
    {

    }
    //other settings
    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        foreach (var entry in ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Added:
                    ((BaseEntity)entry.Entity).AddedDate = DateTime.Now;
                    ((BaseEntity)entry.Entity).LastModified = DateTime.Now;
                    break;

                case EntityState.Modified:
                    ((BaseEntity)entry.Entity).LastModified = DateTime.Now;
                    break;

                case EntityState.Deleted:
                    entry.State = EntityState.Modified;
                    entry.CurrentValues["IsDeleted"] = true;
                    break;
            }
        }
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }
Ryan
  • 19,118
  • 10
  • 37
  • 53
1

I thought you might like to know why you were getting the the events you saw in your question.

When you execute the line student.Name = "New student name";, then, by default, nothing happens because EF Core hasn't called the ChangeTracker.DetectChanges method yet so it doesn't know anything has changed.

But the call to var entry = _dbContext.Entry(student); then runs a version of the ChangeTracker.DetectChanges - see the code below taken from the EF Core code.

public virtual EntityEntry<TEntity> Entry<TEntity>([NotNull] TEntity entity) where TEntity : class
{
    Check.NotNull<TEntity>(entity, nameof (entity));
    this.CheckDisposed();
    EntityEntry<TEntity> entityEntry = this.EntryWithoutDetectChanges<TEntity>(entity);
    //My comment - this runs a version of the DetectChanges method. 
    this.TryDetectChanges((EntityEntry) entityEntry);
    return entityEntry;
}

EF Core's Entry method does this because you might ask for the State of the entity and therefore it has to call DetectChanges to make sure its up to date.

Now, it turns out that if you do the following

student.Name = "New student name";
_dbContext.SaveChanges();

Then (in EF Core 5 preview, but I think it is the same in EF Core 3.1) you get two events.

  1. OldState.EntityState == Unchanged, newState.EntityState == Modified - that is triggered by the call to DetectChanges.
  2. OldState.EntityState == Modified, newState.EntityState == Unchanged - that is triggered by SaveChanges when it set the state to say the database matches the entity class.

If you do the following

student.Name = "New student name";
var entry = _dbContext.Entry(student);
_dbContext.SaveChanges();

Then you would get the same events. The DetectChanges would be called twice (once by Entry and once by SaveChanges), but there is no change in the State on the second call the DetectChanges

You can see this in my unit tests in the repo I am writing to support my book Entity Framework Core in Action. I'm writing the section on these events and found your question and though I would answer it.

I hope it helps you understand what is going on, but I should say that the other answers suggesting overriding SaveChanges is a better solution than using these events.

Jon P Smith
  • 2,391
  • 1
  • 30
  • 55