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

EF events example fails #3267

Open samuelpsfung opened 3 years ago

samuelpsfung commented 3 years ago

I run the example with MySQL, there are logs from the event listener, but the modified timestamp is not persisted to DB.

ef-events


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

ajcvickers commented 3 years ago

@roji to take a look.

roji commented 3 years ago

@ajcvickers this indeed doesn't work (not related to MySQL - see minimal repro below). By the time that the event fires - settings the Modified timestamp - that property has already been checked by the change tracker, and so the property remains Unchanged and the new value doesn't get sent to the database.

Explicitly setting the property to IsModified=true in the event handler fixes the issue. The somewhat tricky thing here is that depending on the traversal order of the properties in the change tracker, this may or may not happen. We should update the guidance here.

(BTW not sure about showing the "deleted" timestamp here, given that the entity is about to get deleted...)

Repro ```c# using var context = new BlogsContext(); context.Database.EnsureDeleted(); context.Database.EnsureCreated(); var blog = context.Blogs.Single(); blog.Name = "EF Core Blog"; context.SaveChanges(); public class BlogsContext : DbContext { public BlogsContext() { ChangeTracker.StateChanged += UpdateTimestamps; ChangeTracker.Tracked += UpdateTimestamps; } private static ILoggerFactory ContextLoggerFactory => LoggerFactory.Create(b => b.AddConsole().AddFilter("", LogLevel.Information)); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseSqlServer(@"Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0") .EnableSensitiveDataLogging() .UseLoggerFactory(ContextLoggerFactory); protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity().HasData(new Blog { Id = 1, Name = "EF Blog" }); public DbSet Blogs { get; set; } private static void UpdateTimestamps(object sender, EntityEntryEventArgs e) { if (e.Entry.Entity is IHasTimestamps entityWithTimestamps) { switch (e.Entry.State) { case EntityState.Deleted: entityWithTimestamps.Deleted = DateTime.UtcNow; // e.Entry.Property("Modified").IsModified = true; Console.WriteLine($"Stamped for delete: {e.Entry.Entity}"); break; case EntityState.Modified: entityWithTimestamps.Modified = DateTime.UtcNow; // e.Entry.Property("Modified").IsModified = true; Console.WriteLine($"Stamped for update: {e.Entry.Entity}"); break; case EntityState.Added: entityWithTimestamps.Added = DateTime.UtcNow; Console.WriteLine($"Stamped for insert: {e.Entry.Entity}"); break; } } } } public static class HasTimestampsExtensions { public static string ToStampString(this IHasTimestamps entity) { return $"{GetStamp("Added", entity.Added)}{GetStamp("Modified", entity.Modified)}{GetStamp("Deleted", entity.Deleted)}"; string GetStamp(string state, DateTime? dateTime) => dateTime == null ? "" : $" {state} on: {dateTime}"; } } public interface IHasTimestamps { DateTime? Added { get; set; } DateTime? Deleted { get; set; } DateTime? Modified { get; set; } } public class Blog : IHasTimestamps { [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } public string Name { get; set; } public DateTime? Added { get; set; } public DateTime? Deleted { get; set; } public DateTime? Modified { get; set; } public override string ToString() => $"Blog {Id}{this.ToStampString()}"; } ²²²