dotnet / EntityFramework.Docs

Documentation for Entity Framework Core and Entity Framework 6
https://docs.microsoft.com/ef/
Creative Commons Attribution 4.0 International
1.62k stars 1.96k forks source link

ChangeTracker's StateChanged event's changes not being updated #3888

Open patrickhacens opened 2 years ago

patrickhacens commented 2 years ago

File a bug

Base on this documentation I expect that when saving changes any property manipulation inside the StatusChanged event would also be included in the update command generated by EF, done some tests and I can't get it to work, and I'm not sure if that is the expected behavior.

Testbed

Below is a test snipped that can be run in a toplevel statement. At the set up value on the database part the ChangedAt property is properly set using the Tracked event.

Simulating normal update entity use case, I retrieve the value from a new context and change its name property, after calling SaveChanges the StatusChanged event is callend and the entity ChangedAt property is updated, but this update is not persisted on the database (or not present in the sql generated when using a SQLServer provider), but still the entity's ChangedAt property is de facto changed.

After that for control I retrieve the same entity again to be sure that the ChangeAt property is not updated in the previous step.

using Microsoft.EntityFrameworkCore;
CancellationTokenSource tks = new CancellationTokenSource();
DateTime firstRetrieval, valueAfterSaveChanges, secondRetrieval; //control variables

//set up a value on the database
using (Db db = new Db())
{
    db.Entities.Add(new Entity()
    {
        Name = Guid.Empty.ToString(),
    });
    await db.SaveChangesAsync(tks.Token);
}

//retrieve its value and change another variable triggering state changed
using (Db db = new Db())
{
    var entity = await db.Entities.FirstOrDefaultAsync(tks.Token);
    firstRetrieval = entity.ChangedAt;

    Thread.Sleep(1000); // wait to have a visible changed date, can be skipped/removed

    entity.Name = Guid.NewGuid().ToString();
    await db.SaveChangesAsync(tks.Token);
    valueAfterSaveChanges = entity.ChangedAt;
}

//retrieve value again
using (Db db = new Db())
{
    var entity = await db.Entities.FirstOrDefaultAsync(tks.Token);
    secondRetrieval  = entity.ChangedAt;
}

Console.WriteLine($"{firstRetrieval} value in first retrieval");
Console.WriteLine($"{valueAfterSaveChanges} value after savechanges, updated by state changed event");
Console.WriteLine($"{secondRetrieval} value in second retrieval, should be equals to {valueAfterSaveChanges} {(secondRetrieval != valueAfterSaveChanges ? "but its not" : "")}");

Thread.Sleep(Timeout.Infinite);

public class Db : DbContext
{
    public DbSet<Entity> Entities { get; set; }
    public Db()
    {
        ChangeTracker.StateChanged += UpdateTimes;
        ChangeTracker.Tracked += UpdateTimes;
    }

    private void UpdateTimes(object sender, Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntryEventArgs e)
    {
        switch (e.Entry.State)
        {
            case EntityState.Modified:
            case EntityState.Added:
                (e.Entry.Entity as Entity).ChangedAt = DateTime.Now;
                break;
        }
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("teste");
        base.OnConfiguring(optionsBuilder);
    }

}
public class Entity
{
    public Guid Id { get; set; }
    public String Name { get; set; }
    public DateTime ChangedAt { get; set; }
}

OUTPUT

info: 15/05/2022 00:03:08.909 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.5 initialized 'Db' using provider 'Microsoft.EntityFrameworkCore.InMemory:6.0.5' with options: StoreName=nomezinho
info: 15/05/2022 00:03:08.979 InMemoryEventId.ChangesSaved[30100] (Microsoft.EntityFrameworkCore.Update)
      Saved 1 entities to in-memory store.
info: 15/05/2022 00:03:08.994 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.5 initialized 'Db' using provider 'Microsoft.EntityFrameworkCore.InMemory:6.0.5' with options: StoreName=nomezinho
info: 15/05/2022 00:03:10.125 InMemoryEventId.ChangesSaved[30100] (Microsoft.EntityFrameworkCore.Update)
      Saved 1 entities to in-memory store.
info: 15/05/2022 00:03:10.141 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
      Entity Framework Core 6.0.5 initialized 'Db' using provider 'Microsoft.EntityFrameworkCore.InMemory:6.0.5' with options: StoreName=nomezinho
15/05/2022 00:03:08 value in first retrieval
15/05/2022 00:03:10 value after savechanges, updated by state changed event
15/05/2022 00:03:08 value in second retrieval, should be equals to 15/05/2022 00:03:10 but its not

Include provider and version information

EF Core version: 6.0.5, and 6.0.4 Database provider: Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.InMemory Target framework: .NET 6.0 Operating system: win10-x64 build 19044.1645

IDE: Microsoft Visual Studio Community 2022 (64-bit) 17.3.0 Preview 1.0

patrickhacens commented 2 years ago

I should also point that calling db.ChangeTracker.Entries(); before the saving changes and after changing a property or inside the SavingChanges event changes the behavior and makes the property changes inside the StateChanged event be persisted correctly.

ajcvickers commented 2 years ago

This issue is lacking enough information for us to be able to fully understand what is happening. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

patrickhacens commented 2 years ago

The code supplied is enough to see what is happening is it not? You can just copy that and paste on a toplevel console application and run, on the console will show the values of the properties mentioned on the console.

It this information is not enough you can tell me what more is needed and I will provide

ajcvickers commented 2 years ago

@patrickhacens There is no code for the DbContext type or entity types.

patrickhacens commented 2 years ago

@ajcvickers Sorry for that, something must have happened when I pasted it, will be uploading shortly

patrickhacens commented 2 years ago

I updated the snippet with DbContext and entity types, if anything else is missing please ping me. If it really is not the intended behavior I would be apt to delve deeper and do a pull request

ajcvickers commented 2 years ago

Note for triage: the issue here is that the event is happening as part of DetectChanges, which means if a property value is set while processing the event, then it won't be detected until DetectChanges happens again. This isn't desirable, but it's not immediately clear to me how to solve this. It's related, but in some respects the opposite of, dotnet/efcore#26506.

ajcvickers commented 2 years ago

We discussed this with the team, and there really doesn't seem to be any way around this. Doing another round of DetectChanges would potentially lead to the need for yet another, and so on, as well as being a big performance hit.

Instead, the way to deal with this is to set the property value in a way that EF knows about it without having to detect it. For example:

switch (e.Entry.State)
{
    case EntityState.Modified:
    case EntityState.Added:
        e.Entry.Property(nameof(Entity.ChangedAt)).CurrentValue = DateTime.Now;
        break;
}

This is something it would be worth noting in the documentation.