Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
patrickhacens opened this issue May 15, 2022 · 8 comments
Open

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

patrickhacens opened this issue May 15, 2022 · 8 comments

Comments

@patrickhacens
Copy link

patrickhacens commented May 15, 2022

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
Copy link
Author

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
Copy link
Contributor

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
Copy link
Author

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
Copy link
Contributor

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

@patrickhacens
Copy link
Author

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

@patrickhacens
Copy link
Author

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
Copy link
Contributor

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
Copy link
Contributor

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.

@ajcvickers ajcvickers transferred this issue from dotnet/efcore Jun 10, 2022
@ajcvickers ajcvickers self-assigned this Jun 10, 2022
@ajcvickers ajcvickers added this to the Backlog milestone Jun 10, 2022
@ajcvickers ajcvickers removed their assignment Aug 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants