diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 8361516677a..4fa300c871a 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -581,20 +581,20 @@ void HandleSharedColumns( static List? FindJsonPartialUpdateInfo(IUpdateEntry entry, List processedEntries) { var result = new List(); - var currentEntry = entry; + IUpdateEntry? currentEntry = entry; var currentOwnership = currentEntry.EntityType.FindOwnership()!; - while (currentEntry.EntityType.IsMappedToJson()) + while (currentEntry is not null && currentEntry.EntityType.IsMappedToJson()) { var jsonPropertyName = currentEntry.EntityType.GetJsonPropertyName()!; currentOwnership = currentEntry.EntityType.FindOwnership()!; var previousEntry = currentEntry; #pragma warning disable EF1001 // Internal EF Core API usage. currentEntry = ((InternalEntityEntry)currentEntry).StateManager.FindPrincipal( - (InternalEntityEntry)currentEntry, currentOwnership)!; + (InternalEntityEntry)currentEntry, currentOwnership); #pragma warning restore EF1001 // Internal EF Core API usage. - if (processedEntries.Contains(currentEntry)) + if (currentEntry == null || processedEntries.Contains(currentEntry)) { return null; } @@ -635,7 +635,7 @@ void HandleSharedColumns( } // parent entity got deleted, no need to do any json-specific processing - if (currentEntry.EntityState == EntityState.Deleted) + if (currentEntry?.EntityState == EntityState.Deleted) { return null; } diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs index 2dd1a172555..5a6c0cd8be3 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs @@ -3732,6 +3732,42 @@ public virtual Task Replace_json_reference_root_preserves_nested_owned_entities_ Assert.NotNull(result.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf); }); + [ConditionalFact] + public virtual Task Replace_derived_entity_with_json_to_base_entity_with_same_key() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var entity = await context.JsonEntitiesInheritance.OfType().SingleAsync(); + context.Remove(entity); + context.Add(new JsonEntityInheritanceBase + { + Id = entity.Id, + Name = "ReplacementBase", + ReferenceOnBase = new JsonOwnedBranch + { + Date = new DateTime(2010, 1, 1), + Fraction = 1.0m, + Enum = JsonEnum.One, + Enums = [JsonEnum.One], + NullableEnums = [null], + OwnedReferenceLeaf = new JsonOwnedLeaf { SomethingSomething = "leaf" }, + OwnedCollectionLeaf = [] + }, + CollectionOnBase = [] + }); + + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var entity = await context.JsonEntitiesInheritance.SingleAsync(x => x.Id == 2); + Assert.IsNotType(entity); + Assert.Equal("ReplacementBase", entity.Name); + }); + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs index eeec57d6bea..a8a747d71b8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs @@ -3299,6 +3299,32 @@ FROM [JsonEntitiesBasic] AS [j] protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); + public override async Task Replace_derived_entity_with_json_to_base_entity_with_same_key() + { + await base.Replace_derived_entity_with_json_to_base_entity_with_same_key(); + + AssertSql( + """ +@p0='[]' (Nullable = false) (Size = 2) +@p1='{"Date":"2010-01-01T00:00:00","Enum":-1,"Enums":[-1],"Fraction":1.0,"Id":0,"NullableEnum":null,"NullableEnums":[null],"OwnedCollectionLeaf":[],"OwnedReferenceLeaf":{"SomethingSomething":"leaf"}}' (Nullable = false) (Size = 194) +@p4='2' +@p2='JsonEntityInheritanceBase' (Nullable = false) (Size = 34) +@p3='ReplacementBase' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesInheritance] SET [CollectionOnBase] = @p0, [ReferenceOnBase] = @p1, [Discriminator] = @p2, [Name] = @p3 +OUTPUT 1 +WHERE [Id] = @p4; +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[Discriminator], [j].[Name], [j].[Fraction], [j].[CollectionOnBase], [j].[ReferenceOnBase], [j].[CollectionOnDerived], [j].[ReferenceOnDerived] +FROM [JsonEntitiesInheritance] AS [j] +WHERE [j].[Id] = 2 +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }