efcore
efcore copied to clipboard
A delete/insert converted to a update allows changing property values that should not changed after insert
Originally reported here: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1908 See #30705
The main issue here is that the application is marking an entity as Deleted, and then adding a new instance with the same key as Added. Because of #30705, this gets converted to an update containing this:
UPDATE [Item] SET [CreatedAt] = @p0, [UpdatedAt] = @p1
Before 7.0.3, the behavior was this:
UPDATE [Item] SET [UpdatedAt] = @p1
UpdatedAt is marked as BeforeSaveBehavior ignore.
CreatedAt is marked as AfterSaveBehavior ignore.
So, if we treat this as an update, then UpdatedAt should be sent, but CreatedAt should not.
On the other hand, if this remains a delete insert, then CreatedAt should be sent in the insert, and UpdatedAt should not.
Repro:
using var context = new StoreContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
var item = new Item { Id = "abc", CreatedAt = DateTime.Now.ToString() };
var store = new Store { CreatedAt = DateTime.Now.ToString(), Items = { item }};
context.Add(store);
await context.SaveChangesAsync();
context.ChangeTracker.Clear();
store = await context.Stores.Include(e => e.Items).SingleAsync();
store.CreatedAt = "X";
store.UpdatedAt = "Y";
store.Items = new List<Item>()
{ new() { Id = item.Id, CreatedAt = "A", UpdatedAt = "B" } };
// Doing the store update like this instead does not result in the issue:
// store.Items.First().CreatedAt += "+";
// store.Items.First().UpdatedAt += "+";
context.SaveChanges();
public class Item
{
public string Id { get; set; } = null!;
public int StoreId { get; set; }
public string? CreatedAt { get; set; }
public string? UpdatedAt { get; set; }
}
public class Store
{
public int StoreId { get; set; }
public List<Item> Items { get; set; } = new();
public string? CreatedAt { get; set; }
public string? UpdatedAt { get; set; }
}
public class StoreContext : DbContext
{
public DbSet<Store> Stores => Set<Store>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder
.LogTo(Console.WriteLine, LogLevel.Information)
.UseSqlServer(
"Data Source=localhost;Database=BuildBlogs;Integrated Security=True;Trust Server Certificate=True;ConnectRetryCount=0");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Item>(b =>
{
b.Property(x => x.CreatedAt).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
b.Property(x => x.UpdatedAt).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);
});
modelBuilder.Entity<Store>(b =>
{
b.Property(x => x.CreatedAt).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
b.Property(x => x.UpdatedAt).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Ignore);
});
}
}