diff --git a/src/EFCore.Design/Design/Internal/DbContextOperations.cs b/src/EFCore.Design/Design/Internal/DbContextOperations.cs index 3daa71f0220..4fab5251b87 100644 --- a/src/EFCore.Design/Design/Internal/DbContextOperations.cs +++ b/src/EFCore.Design/Design/Internal/DbContextOperations.cs @@ -336,7 +336,10 @@ private IReadOnlyList 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 { ["_EFGenerationStage"] = "build" }); workspace.LoadMetadataForReferencedProjects = true; #pragma warning disable CS0612 // Obsolete #pragma warning disable CS0618 // Obsolete diff --git a/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets index 8989c09df04..15f0b4d2804 100644 --- a/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets +++ b/src/EFCore.Tasks/buildTransitive/Microsoft.EntityFrameworkCore.Tasks.targets @@ -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 --> + { + using (SuspendRecordingEvents()) + { + var widget = await context.Set().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()); @@ -607,6 +656,7 @@ protected virtual IDisposable SuspendRecordingEvents() protected class ComplexCollectionJsonContext(DbContextOptions options) : DbContext(options) { public DbSet Companies { get; set; } = null!; + public DbSet Widgets { get; set; } = null!; } protected class CompanyWithComplexCollections @@ -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 Items { get; set; } = []; + } + + protected class DeepItem + { + public required string Title { get; set; } + public List Inner { get; set; } = []; + public List Others { get; set; } = []; + } + + protected class InnerEntry + { + public required string Value { get; set; } + } + public abstract class ComplexCollectionJsonUpdateFixtureBase : SharedStoreFixtureBase { protected override string StoreName @@ -660,7 +738,8 @@ protected override Type ContextType => typeof(ComplexCollectionJsonContext); protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - => modelBuilder.Entity(b => + { + modelBuilder.Entity(b => { b.Property(x => x.Id).ValueGeneratedNever(); @@ -686,6 +765,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con }); }); + modelBuilder.Entity(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 @@ -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(); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Update/ComplexCollectionJsonUpdateSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Update/ComplexCollectionJsonUpdateSqliteTest.cs index 0c4bcd5f64f..7d524fb0fa7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Update/ComplexCollectionJsonUpdateSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Update/ComplexCollectionJsonUpdateSqliteTest.cs @@ -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