Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 4 additions & 1 deletion src/EFCore.Design/Design/Internal/DbContextOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,10 @@ private IReadOnlyList<string> PrecompileQueries(

try
{
workspace = MSBuildWorkspace.Create();
// Set _EFGenerationStage to a non-empty value so that the design-time build performed by
// OpenProjectAsync below doesn't re-trigger the EF file generation targets. Otherwise the
// generation targets would invoke this operation again, resulting in a fork bomb.
workspace = MSBuildWorkspace.Create(new Dictionary<string, string> { ["_EFGenerationStage"] = "build" });
workspace.LoadMetadataForReferencedProjects = true;
#pragma warning disable CS0612 // Obsolete
#pragma warning disable CS0618 // Obsolete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ For Publish:
This target has the same Inputs and Outputs as CoreCompile to run only if CoreCompile isn't going to be skipped -->
<Target Name="_EFPrepareForCompile"
DependsOnTargets="_EFProcessGeneratedFiles"
Condition="'$(_EFGenerationStage)'==''"
Condition="'$(_EFGenerationStage)'=='' And '$(DesignTimeBuild)' != 'True'"
Inputs="$(MSBuildAllProjects);
@(Compile);
@(_CoreCompileResourceInputs);
Expand Down
12 changes: 11 additions & 1 deletion src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,9 +637,19 @@ public void SetPropertyModified(

if (recurse)
{
var newElementState = isModified ? EntityState.Modified : EntityState.Unchanged;
foreach (var complexEntry in GetFlattenedComplexEntries())
{
complexEntry.SetEntityState(isModified ? EntityState.Modified : EntityState.Unchanged, modifyProperties: true);
// Added elements represent pending additions with no original ordinal, so forcing them to
// Modified/Unchanged is incorrect and would fail the original ordinal validation. Leave their
// state (computed by change detection) untouched, mirroring the bulk state-change logic in
// InternalComplexCollectionEntry.SetState.
if (complexEntry.EntityState is EntityState.Added)
{
continue;
}

complexEntry.SetEntityState(newElementState, modifyProperties: true);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,55 @@ public virtual Task Replace_complex_property_mapped_to_json()
}
});

[Fact]
public virtual Task Grow_nested_sub_collection_in_complex_property_mapped_to_json()
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
CreateContext,
UseTransaction,
async context =>
{
var widget = await context.Set<WidgetWithDeepJson>().OrderBy(w => w.Id).FirstAsync();

// Replace the entire JSON-mapped complex property with a structure where one nested
// sub-collection (Inner) has grown from one element to two. The sibling sub-collection
// (Others) being present in the type definition is required to trigger the regression.
widget.Deep = new DeepData
{
Mid = new MiddleData
{
Items =
[
new DeepItem
{
Title = "Item1",
Inner =
[
new InnerEntry { Value = "inner-0" },
new InnerEntry { Value = "inner-1" }
],
Others = []
}
]
}
};

ClearLog();
await context.SaveChangesAsync();
},
async context =>
{
using (SuspendRecordingEvents())
{
var widget = await context.Set<WidgetWithDeepJson>().OrderBy(w => w.Id).FirstAsync();
var item = Assert.Single(widget.Deep.Mid.Items);
Assert.Equal("Item1", item.Title);
Assert.Equal(2, item.Inner.Count);
Assert.Equal("inner-0", item.Inner[0].Value);
Assert.Equal("inner-1", item.Inner[1].Value);
Assert.Empty(item.Others);
}
});

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

Expand All @@ -607,6 +656,7 @@ protected virtual IDisposable SuspendRecordingEvents()
protected class ComplexCollectionJsonContext(DbContextOptions options) : DbContext(options)
{
public DbSet<CompanyWithComplexCollections> Companies { get; set; } = null!;
public DbSet<WidgetWithDeepJson> Widgets { get; set; } = null!;
}

protected class CompanyWithComplexCollections
Expand Down Expand Up @@ -645,6 +695,34 @@ protected class Department
public decimal Budget { get; set; }
}

protected class WidgetWithDeepJson
{
public int Id { get; set; }
public required DeepData Deep { get; set; }
}

protected class DeepData
{
public required MiddleData Mid { get; set; }
}

protected class MiddleData
{
public List<DeepItem> Items { get; set; } = [];
}

protected class DeepItem
{
public required string Title { get; set; }
public List<InnerEntry> Inner { get; set; } = [];
public List<InnerEntry> Others { get; set; } = [];
}

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

public abstract class ComplexCollectionJsonUpdateFixtureBase : SharedStoreFixtureBase<DbContext>
{
protected override string StoreName
Expand All @@ -660,7 +738,8 @@ protected override Type ContextType
=> typeof(ComplexCollectionJsonContext);

protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
=> modelBuilder.Entity<CompanyWithComplexCollections>(b =>
{
modelBuilder.Entity<CompanyWithComplexCollections>(b =>
{
b.Property(x => x.Id).ValueGeneratedNever();

Expand All @@ -686,6 +765,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
});
});

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

b.ComplexProperty(
x => x.Deep, db =>
{
db.ToJson();
db.ComplexProperty(
d => d.Mid, mb =>
mb.ComplexCollection(
m => m.Items, ib =>
{
ib.ComplexCollection(i => i.Inner);
ib.ComplexCollection(i => i.Others);
}));
});
});
}

protected override Task SeedAsync(DbContext context)
{
var company = new CompanyWithComplexCollections
Expand Down Expand Up @@ -716,6 +815,28 @@ protected override Task SeedAsync(DbContext context)
};

context.Add(company);

var widget = new WidgetWithDeepJson
{
Id = 1,
Deep = new DeepData
{
Mid = new MiddleData
{
Items =
[
new DeepItem
{
Title = "Item1",
Inner = [new InnerEntry { Value = "inner-0" }],
Others = []
}
]
}
}
};

context.Add(widget);
return context.SaveChangesAsync();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,21 @@ public override async Task Replace_complex_property_mapped_to_json()
""");
}

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

AssertSql(
"""
@p0='{"Mid":{"Items":[{"Title":"Item1","Inner":[{"Value":"inner-0"},{"Value":"inner-1"}],"Others":[]}]}}' (Nullable = false) (Size = 99)
@p1='1'

UPDATE "Widgets" SET "Deep" = @p0
WHERE "Id" = @p1
RETURNING 1;
""");
}

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