diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 2412ca0c769..d12bfca101e 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -1149,12 +1149,28 @@ private void SetProperty( { WritePropertyValue(propertyBase, value, isMaterialization); - if (currentValueType != CurrentValueType.Normal - && !_temporaryValues.IsEmpty) + var useNewBehavior + = !AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue19137", out var isEnabled) || !isEnabled; + + if (useNewBehavior) { - var defaultValue = asProperty.ClrType.GetDefaultValue(); - var storeGeneratedIndex = asProperty.GetStoreGeneratedIndex(); - _temporaryValues.SetValue(asProperty, defaultValue, storeGeneratedIndex); + if (currentValueType != CurrentValueType.Normal + && !_temporaryValues.IsEmpty) + { + var defaultValue = asProperty.ClrType.GetDefaultValue(); + var storeGeneratedIndex = asProperty.GetStoreGeneratedIndex(); + _temporaryValues.SetValue(asProperty, defaultValue, storeGeneratedIndex); + } + } + else + { + if (currentValueType != CurrentValueType.Normal + && !_temporaryValues.IsEmpty + && equals(value, asProperty.ClrType.GetDefaultValue())) + { + var storeGeneratedIndex = asProperty.GetStoreGeneratedIndex(); + _temporaryValues.SetValue(asProperty, value, storeGeneratedIndex); + } } } else diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index bd02465d7b8..f5e143446d9 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -960,6 +960,7 @@ public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumer { var doCascadeDelete = force || CascadeDeleteTiming != CascadeTiming.Never; var principalIsDetached = entry.EntityState == EntityState.Detached; + var useNewBehavior = !AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue18982", out var isEnabled) || !isEnabled; foreignKeys ??= entry.EntityType.GetReferencingForeignKeys(); foreach (var fk in foreignKeys) @@ -981,10 +982,14 @@ public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumer || fk.DeleteBehavior == DeleteBehavior.ClientCascade) && doCascadeDelete) { - var cascadeState = principalIsDetached - || dependent.EntityState == EntityState.Added + var cascadeState = useNewBehavior + ? (principalIsDetached + || dependent.EntityState == EntityState.Added + ? EntityState.Detached + : EntityState.Deleted) + : (dependent.EntityState == EntityState.Added ? EntityState.Detached - : EntityState.Deleted; + : EntityState.Deleted); if (SensitiveLoggingEnabled) { @@ -999,7 +1004,8 @@ public virtual void CascadeDelete(InternalEntityEntry entry, bool force, IEnumer CascadeDelete(dependent, force); } - else if (!principalIsDetached) + else if (!useNewBehavior + || !principalIsDetached) { foreach (var dependentProperty in fk.Properties) { diff --git a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs index 2c51c06bab8..c719d3e6af3 100644 --- a/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs +++ b/test/EFCore.Specification.Tests/PropertyValuesTestBase.cs @@ -1262,53 +1262,53 @@ public async Task Values_can_be_reloaded_from_database_for_entity_in_any_state(E [InlineData(EntityState.Detached, false)] public async Task Reload_when_entity_deleted_in_store_can_happen_for_any_state(EntityState state, bool async) { - using (var context = CreateContext()) - { - var office = new Office { Number = "35" }; - var mailRoom = new MailRoom { id = 36 }; - var building = Building.Create(Guid.NewGuid(), "Bag End", 77); + using var context = CreateContext(); - building.Offices.Add(office); - building.PrincipalMailRoom = mailRoom; - office.Building = building; - mailRoom.Building = building; + var office = new Office { Number = "35" }; + var mailRoom = new MailRoom { id = 36 }; + var building = Building.Create(Guid.NewGuid(), "Bag End", 77); - var entry = context.Entry(building); + building.Offices.Add(office); + building.PrincipalMailRoom = mailRoom; + office.Building = building; + mailRoom.Building = building; - context.Attach(building); - entry.State = state; + var entry = context.Entry(building); - if (async) - { - await entry.ReloadAsync(); - } - else - { - entry.Reload(); - } + context.Attach(building); + entry.State = state; - Assert.Equal("Bag End", entry.Property(e => e.Name).OriginalValue); - Assert.Equal("Bag End", entry.Property(e => e.Name).CurrentValue); - Assert.Equal("Bag End", building.Name); - - if (state == EntityState.Added) - { - Assert.Equal(EntityState.Added, entry.State); - Assert.Same(mailRoom, building.PrincipalMailRoom); - Assert.Contains(office, building.Offices); - } - else - { - Assert.Equal(EntityState.Detached, entry.State); - Assert.Null(mailRoom.Building); + if (async) + { + await entry.ReloadAsync(); + } + else + { + entry.Reload(); + } - Assert.Equal(EntityState.Detached, context.Entry(office.Building).State); - Assert.Same(building, office.Building); - } + Assert.Equal("Bag End", entry.Property(e => e.Name).OriginalValue); + Assert.Equal("Bag End", entry.Property(e => e.Name).CurrentValue); + Assert.Equal("Bag End", building.Name); + if (state == EntityState.Added) + { + Assert.Equal(EntityState.Added, entry.State); Assert.Same(mailRoom, building.PrincipalMailRoom); Assert.Contains(office, building.Offices); } + else + { + Assert.Equal(EntityState.Detached, entry.State); + Assert.Same(mailRoom, building.PrincipalMailRoom); + Assert.Contains(office, building.Offices); + + Assert.Equal(EntityState.Detached, context.Entry(office.Building).State); + Assert.Same(building, office.Building); + } + + Assert.Same(mailRoom, building.PrincipalMailRoom); + Assert.Contains(office, building.Offices); } [ConditionalFact] diff --git a/test/EFCore.Tests/ChangeTracking/Internal/FixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/FixupTest.cs index fe6407f136c..807e3738d86 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/FixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/FixupTest.cs @@ -2888,209 +2888,6 @@ public void Detached_entity_is_not_replaced_by_tracked_entity() } } - public class ContainerX - { - public int Id { get; set; } - public string Name { get; set; } - public List Rooms { get; set; } = new List(); - } - - public class ContainerRoomX - { - public int Id { get; set; } - public int Number { get; set; } - public int ContainerId { get; set; } - public ContainerX Container { get; set; } - public int? ProductId { get; set; } - public ProductX Product { get; set; } - } - - public class ProductX - { - public int Id { get; set; } - public string Description { get; set; } - public List Rooms { get; set; } = new List(); - } - - protected class EscapeRoom : DbContext - { - private readonly string _databaseName; - - public EscapeRoom(string databaseName) - { - _databaseName = databaseName; - } - - protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseInMemoryDatabase(_databaseName); - - protected internal override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .HasOne(room => room.Product) - .WithMany(product => product.Rooms) - .HasForeignKey(room => room.ProductId) - .IsRequired(false) - .OnDelete(DeleteBehavior.Cascade); - } - } - - [ConditionalFact] - public void Replaced_duplicate_entities_are_used_even_with_bad_hash() - { - using (var context = new BadHashDay("BadHashDay")) - { - context.AddRange( - new ParentX { Id = 101, Name = "Parent1" }, - new ChildX { Id = 201, Name = "Child1" }, - new ParentChildX - { - ParentId = 101, - ChildId = 201, - SortOrder = 1 - }); - - context.SaveChanges(); - } - - using (var context = new BadHashDay("BadHashDay")) - { - var parent = context.Set().Single(x => x.Id == 101); - var join = context.Set().Single(); - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - Assert.Equal(EntityState.Unchanged, context.Entry(parent).State); - Assert.Equal(EntityState.Unchanged, context.Entry(join).State); - - parent.ParentChildren.Clear(); - - var newJoin = new ParentChildX - { - ParentId = 101, - ChildId = 201, - SortOrder = 1 - }; - - parent.ParentChildren = new List { newJoin }; - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - Assert.Equal(EntityState.Unchanged, context.Entry(parent).State); - Assert.Equal(EntityState.Deleted, context.Entry(join).State); - Assert.Equal(EntityState.Added, context.Entry(newJoin).State); - - context.SaveChanges(); - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - Assert.Equal(EntityState.Unchanged, context.Entry(parent).State); - Assert.Equal(EntityState.Detached, context.Entry(join).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newJoin).State); - } - } - - protected class ParentX - { - public int Id { get; set; } - public string Name { get; set; } - public virtual IList ParentChildren { get; set; } = new List(); - } - - protected class ParentChildX - { - public int ParentId { get; set; } - public int ChildId { get; set; } - public int SortOrder { get; set; } - public virtual ParentX Parent { get; set; } - public virtual ChildX Child { get; set; } - - // Bad implementation of Equals to test for regression - public override bool Equals(object obj) - { - if (obj == null) - { - return false; - } - - var other = (ParentChildX)obj; - - if (!Equals(ParentId, other.ParentId)) - { - return false; - } - - if (!Equals(ChildId, other.ChildId)) - { - return false; - } - - return true; - } - - // Bad implementation of GetHashCode to test for regression - public override int GetHashCode() - { - var hashCode = 13; - hashCode = (hashCode * 7) + ParentId.GetHashCode(); - hashCode = (hashCode * 7) + ChildId.GetHashCode(); - return hashCode; - } - } - - protected class ChildX - { - public int Id { get; set; } - public string Name { get; set; } - public virtual IList ParentChildren { get; set; } = new List(); - } - - protected class BadHashDay : DbContext - { - private readonly string _databaseName; - - public BadHashDay(string databaseName) - { - _databaseName = databaseName; - } - - protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseInMemoryDatabase(_databaseName); - - protected internal override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity() - .HasMany(x => x.ParentChildren) - .WithOne(op => op.Parent) - .IsRequired(); - - modelBuilder.Entity() - .HasMany(x => x.ParentChildren) - .WithOne(op => op.Child) - .IsRequired(); - - modelBuilder.Entity().HasKey( - x => new { x.ParentId, x.ChildId }); - } - } - - [ConditionalFact] - public void Detached_entity_is_not_replaced_by_tracked_entity() - { - using (var context = new BadBeeContext(nameof(BadBeeContext))) - { - var b1 = new EntityB { EntityBId = 1 }; - context.BEntities.Attach(b1); - - var b2 = new EntityB { EntityBId = 1 }; - - var a = new EntityA { EntityAId = 1, EntityB = b2 }; - - Assert.Equal( - CoreStrings.IdentityConflict( - nameof(EntityB), - $"{{'{nameof(EntityB.EntityBId)}'}}"), - Assert.Throws(() => context.Add(a)).Message); - } - } - private class EntityB { public int EntityBId { get; set; } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs index e253b9cf49c..3da420405aa 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/OwnedFixupTest.cs @@ -3400,78 +3400,64 @@ public void Parent_and_identity_swapped_bidirectional_collection(EntityState ent [InlineData(true)] public void Fixup_works_when_changing_state_from_Detached_to_Modified(bool detachDependent) { - using (var context = new OwnedModifiedContext(Guid.NewGuid().ToString())) - { - var details = new ProductDetails { Color = "C1", Size = "S1" }; + using var context = new OwnedModifiedContext(Guid.NewGuid().ToString()); - var product = new Product { Name = "Product1", Details = details }; + var details = new ProductDetails { Color = "C1", Size = "S1" }; + var product = new Product { Name = "Product1", Details = details }; - context.Add(product); - context.SaveChanges(); + context.Add(product); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - Assert.Equal(EntityState.Unchanged, context.Entry(product).State); - Assert.Equal(EntityState.Unchanged, context.Entry(details).State); + Assert.True(context.ChangeTracker.HasChanges()); - context.Entry(product).State = EntityState.Detached; - if (detachDependent) - { - context.Entry(details).State = EntityState.Detached; - } + context.SaveChanges(); - if (detachDependent) - { - Assert.Empty(context.ChangeTracker.Entries()); - } - else - { - Assert.Single(context.ChangeTracker.Entries()); - Assert.Equal(EntityState.Deleted, context.Entry(details).State); - } + Assert.False(context.ChangeTracker.HasChanges()); - var newDetails = new ProductDetails { Color = "C2", Size = "S2" }; + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(EntityState.Unchanged, context.Entry(product).State); + Assert.Equal(EntityState.Unchanged, context.Entry(details).State); - var newProduct = new Product - { - Id = product.Id, - Name = "Product1NewName", - Details = newDetails - }; + context.Entry(product).State = EntityState.Detached; + if (detachDependent) + { + context.Entry(details).State = EntityState.Detached; + } - context.Update(newProduct); + Assert.False(context.ChangeTracker.HasChanges()); - if (detachDependent) - { - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - } - else - { - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - Assert.Equal(EntityState.Deleted, context.Entry(details).State); - } + Assert.Empty(context.ChangeTracker.Entries()); + Assert.Equal(EntityState.Detached, context.Entry(details).State); - Assert.Equal(EntityState.Modified, context.Entry(newProduct).State); - Assert.Equal(EntityState.Modified, context.Entry(newDetails).State); + var newDetails = new ProductDetails { Color = "C2", Size = "S2" }; - Assert.Same(details, product.Details); - Assert.Equal("C1", product.Details.Color); - Assert.Same(newDetails, newProduct.Details); - Assert.Equal("C2", newProduct.Details.Color); + var newProduct = new Product + { + Id = product.Id, + Name = "Product1NewName", + Details = newDetails + }; - if (detachDependent) - { - context.SaveChanges(); + context.Update(newProduct); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - Assert.Equal(EntityState.Unchanged, context.Entry(newProduct).State); - Assert.Equal(EntityState.Unchanged, context.Entry(newDetails).State); - } - else - { - // Because attempting to update an entity after it has been deleted - Assert.Throws(() => context.SaveChanges()); - } - } + Assert.True(context.ChangeTracker.HasChanges()); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + Assert.Equal(EntityState.Modified, context.Entry(newProduct).State); + Assert.Equal(EntityState.Modified, context.Entry(newDetails).State); + + Assert.Same(details, product.Details); + Assert.Equal("C1", product.Details.Color); + Assert.Same(newDetails, newProduct.Details); + Assert.Equal("C2", newProduct.Details.Color); + + context.SaveChanges(); + + Assert.False(context.ChangeTracker.HasChanges()); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(EntityState.Unchanged, context.Entry(newProduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(newDetails).State); } private class Product