Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ private struct InternalComplexCollectionEntry(InternalEntryBase entry, IComplexP
private static readonly bool UseOldBehavior37585 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37585", out var enabled) && enabled;

private static readonly bool UseOldBehavior38632 =
Comment thread
AndriySvyryd marked this conversation as resolved.
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue38632", out var enabled) && enabled;

private List<InternalComplexEntry?>? _entries;
private List<InternalComplexEntry?>? _originalEntries;
private bool _isModified;
Expand Down Expand Up @@ -257,12 +260,6 @@ public InternalComplexEntry GetEntry(int ordinal, bool original = false)
ordinal, _complexCollection.DeclaringType.ShortNameChain(), _complexCollection.Name));
}

if (_containingEntry.GetOriginalValue(_complexCollection) == null)
{
throw new InvalidOperationException(
CoreStrings.ComplexCollectionEntryOriginalNull(
_complexCollection.DeclaringType.ShortNameChain(), _complexCollection.Name));
}
}
else
{
Expand All @@ -273,6 +270,29 @@ public InternalComplexEntry GetEntry(int ordinal, bool original = false)
ordinal, _complexCollection.DeclaringType.ShortNameChain(), _complexCollection.Name));
}

}

// Must check tracked entries first to allow reindexing during cleanup when the parent is null.
var existingEntries = original ? _originalEntries : _entries;
if (!UseOldBehavior38632
&& existingEntries != null
&& (uint)ordinal < (uint)existingEntries.Count
&& existingEntries[ordinal] is { } existingEntry)
{
return existingEntry;
}

if (original)
{
if (_containingEntry.GetOriginalValue(_complexCollection) == null)
{
throw new InvalidOperationException(
CoreStrings.ComplexCollectionEntryOriginalNull(
_complexCollection.DeclaringType.ShortNameChain(), _complexCollection.Name));
}
}
else
{
if (_containingEntry[_complexCollection] == null)
{
throw new InvalidOperationException(
Expand All @@ -292,10 +312,10 @@ public InternalComplexEntry GetEntry(int ordinal, bool original = false)
ordinal, _complexCollection.DeclaringType.ShortNameChain(), _complexCollection.Name, entries.Count));
}

var complexEntry = entries[ordinal];
if (complexEntry != null)
if (UseOldBehavior38632
&& entries[ordinal] is { } existingEntryAfterValidation)
{
return complexEntry;
return existingEntryAfterValidation;
}

// The currentEntry is created in Detached state, so it's not added to the entries list yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,30 @@ public virtual Task Grow_nested_sub_collection_in_complex_property_mapped_to_jso
}
});

[ConditionalFact]
public virtual Task Set_nullable_complex_property_with_nested_collection_to_null()
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
CreateContext,
UseTransaction,
async context =>
{
var entity = await context.Set<EntityWithNullableMeta>().OrderBy(e => e.Id).FirstAsync();

entity.Items[0].Meta = null;

ClearLog();
await context.SaveChangesAsync();
},
async context =>
{
using (SuspendRecordingEvents())
{
var entity = await context.Set<EntityWithNullableMeta>().OrderBy(e => e.Id).FirstAsync();
Assert.Single(entity.Items);
Assert.Null(entity.Items[0].Meta);
}
});

protected virtual void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
=> facade.UseTransaction(transaction.GetDbTransaction());

Expand All @@ -657,6 +681,7 @@ protected class ComplexCollectionJsonContext(DbContextOptions options) : DbConte
{
public DbSet<CompanyWithComplexCollections> Companies { get; set; } = null!;
public DbSet<WidgetWithDeepJson> Widgets { get; set; } = null!;
public DbSet<EntityWithNullableMeta> EntitiesWithNullableMeta { get; set; } = null!;
}

protected class CompanyWithComplexCollections
Expand Down Expand Up @@ -723,6 +748,28 @@ protected class InnerEntry
public required string Value { get; set; }
}

protected class EntityWithNullableMeta
{
public int Id { get; set; }
public List<ItemWithMeta> Items { get; set; } = [];
}

protected class ItemWithMeta
{
public required string Name { get; set; }
public OptionalMeta? Meta { get; set; }
}

protected class OptionalMeta
{
public List<MetaEntry> Entries { get; set; } = [];
}

protected class MetaEntry
{
public required string Value { get; set; }
}

public abstract class ComplexCollectionJsonUpdateFixtureBase : SharedStoreFixtureBase<DbContext>
{
protected override string StoreName
Expand Down Expand Up @@ -783,6 +830,20 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
}));
});
});

modelBuilder.Entity<EntityWithNullableMeta>(b =>
{
b.Property(x => x.Id).ValueGeneratedNever();

b.ComplexCollection(
x => x.Items, ib =>
{
ib.ToJson();
ib.ComplexProperty(
x => x.Meta, mb =>
mb.ComplexCollection(m => m.Entries));
});
});
}

protected override Task SeedAsync(DbContext context)
Expand Down Expand Up @@ -837,6 +898,28 @@ protected override Task SeedAsync(DbContext context)
};

context.Add(widget);

var entityWithNullableMeta = new EntityWithNullableMeta
{
Id = 1,
Items =
[
new ItemWithMeta
{
Name = "Item1",
Meta = new OptionalMeta
{
Entries =
[
new MetaEntry { Value = "entry-0" },
new MetaEntry { Value = "entry-1" }
]
}
}
]
};

context.Add(entityWithNullableMeta);
return context.SaveChangesAsync();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,23 @@ OUTPUT 1
""");
}

public override async Task Set_nullable_complex_property_with_nested_collection_to_null()
Comment thread
AndriySvyryd marked this conversation as resolved.
{
await base.Set_nullable_complex_property_with_nested_collection_to_null();

AssertSql(
"""
@p0='[{"Name":"Item1","Meta":null}]' (Nullable = false) (Size = 30)
@p1='1'

SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
UPDATE [EntitiesWithNullableMeta] SET [Items] = @p0
OUTPUT 1
WHERE [Id] = @p1;
""");
}

public class ComplexCollectionJsonUpdateSqlServerFixture : ComplexCollectionJsonUpdateFixtureBase
{
protected override ITestStoreFactory TestStoreFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,21 @@ public override async Task Grow_nested_sub_collection_in_complex_property_mapped
""");
}

public override async Task Set_nullable_complex_property_with_nested_collection_to_null()
{
await base.Set_nullable_complex_property_with_nested_collection_to_null();

AssertSql(
"""
@p0='[{"Name":"Item1","Meta":null}]' (Nullable = false) (Size = 30)
@p1='1'

UPDATE "EntitiesWithNullableMeta" SET "Items" = @p0
WHERE "Id" = @p1
RETURNING 1;
""");
}

public class ComplexCollectionJsonUpdateSqliteFixture : ComplexCollectionJsonUpdateFixtureBase
{
protected override ITestStoreFactory TestStoreFactory
Expand Down
Loading