EntityFramework.Docs
EntityFramework.Docs copied to clipboard
EF events example fails
I run the example with MySQL, there are logs from the event listener, but the modified timestamp is not persisted to DB.
Document Details
⚠ Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.
- ID: 329736f5-8463-ec7f-ead6-9ded03dd6f6b
- Version Independent ID: 89098121-5603-26ba-da4d-331aea51f237
- Content: .NET events - EF Core
- Content Source: entity-framework/core/logging-events-diagnostics/events.md
- Product: entity-framework
- Technology: entity-framework-core
- GitHub Login: @ajcvickers
- Microsoft Alias: avickers
@roji to take a look.
@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
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<Blog>().HasData(new Blog { Id = 1, Name = "EF Blog" });
public DbSet<Blog> 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()}";
}
²²²