diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 4ecb0c6543..b31eff690a 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -51,11 +51,14 @@ jobs: timeout-minutes: 50 if: ${{ !github.event.pull_request.draft }} steps: + - uses: jlumbroso/free-disk-space@main + - uses: PSModule/install-powershell@v1 + with: + Version: latest - uses: actions/checkout@v2 - uses: actions/setup-dotnet@master with: dotnet-version: 9.0.100 - - name: Build All run: ./build-all.ps1 working-directory: ./build diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index e91d08b8c9..7fa051dd53 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -268,7 +268,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) { var ignoredEntity = EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.Any(selector => selector.Predicate(entityEntry.Entity.GetType())); - var onlyForeignKeyModifiedEntity = entityEntry.State == EntityState.Modified && entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey()); + var onlyForeignKeyModifiedEntity = entityEntry.State == EntityState.Modified && IsOnlyForeignKeysModified(entityEntry); if ((entityEntry.State == EntityState.Unchanged && ignoredEntity) || onlyForeignKeyModifiedEntity && ignoredEntity) { continue; @@ -292,7 +292,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } else if (entityEntry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { - if (entityEntry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey())) + if (IsOnlyForeignKeysModified(entityEntry)) { // Skip `PublishEntityDeletedEvent/PublishEntityUpdatedEvent` if only foreign keys have changed. break; @@ -428,7 +428,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, case EntityState.Modified: if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { - if (entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey())) + if (IsOnlyForeignKeysModified(entry)) { // Skip `PublishEntityDeletedEvent/PublishEntityUpdatedEvent` if only foreign keys have changed. break; @@ -472,6 +472,12 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } + protected virtual bool IsOnlyForeignKeysModified(EntityEntry entry) + { + return entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && + (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString())); + } + protected virtual void HandlePropertiesBeforeSave() { var entries = ChangeTracker.Entries().ToList(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs index b102396cb0..8aeeb2cfac 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs @@ -28,7 +28,7 @@ public class AbpAuditingTestDbContext : AbpDbContext public DbSet AppEntityWithValueObject { get; set; } public DbSet AppEntityWithNavigations { get; set; } - + public DbSet AppEntityWithNavigationChildOneToMany { get; set; } public AbpAuditingTestDbContext(DbContextOptions options) : base(options) { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index 22c0b92477..5f905ca062 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -58,6 +58,11 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule { opt.DefaultWithDetailsFunc = q => q.Include(p => p.BlogPosts); }); + + options.Entity(opt => + { + opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToMany); + }); }); context.Services.AddAbpDbContext(options => diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs index 6249d588f5..2967e15f16 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs @@ -48,6 +48,7 @@ public class AbpEntityChangeOptions_DomainEvents_IgnoreEntityChangeSelectorList_ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase { protected readonly IRepository AppEntityWithNavigationsRepository; + protected readonly IRepository AppEntityWithNavigationChildOneToManyRepository; protected readonly ILocalEventBus LocalEventBus; protected readonly IRepository PersonRepository; protected bool _loadEntityWithoutDetails = false; @@ -55,6 +56,7 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase public AbpEfCoreDomainEvents_Tests() { AppEntityWithNavigationsRepository = GetRequiredService>(); + AppEntityWithNavigationChildOneToManyRepository = GetRequiredService>(); LocalEventBus = GetRequiredService(); PersonRepository = GetRequiredService>(); } @@ -357,6 +359,22 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase var entityId = Guid.NewGuid(); await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity") + { + OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany(Guid.NewGuid()) + { + ChildName = "ChildName1" + }, + new AppEntityWithNavigationChildOneToMany(Guid.NewGuid()) + { + ChildName = "ChildName2" + } + } + }); + + var entityId2 = Guid.NewGuid(); + await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId2, "TestEntity") { OneToMany = new List() { @@ -367,6 +385,33 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase } }); + var oneToManyEntity = Guid.NewGuid(); + await AppEntityWithNavigationChildOneToManyRepository.InsertAsync( + new AppEntityWithNavigationChildOneToMany(oneToManyEntity) + { + AppEntityWithNavigationId = entityId, + }); + + LocalEventBus.Subscribe>(data => + { + data.Entity.AppEntityWithNavigationId.ShouldBe(entityId2); + return Task.CompletedTask; + }); + + using (var scope = ServiceProvider.CreateScope()) + { + var uowManager = scope.ServiceProvider.GetRequiredService(); + using (var uow = uowManager.Begin()) + { + var entity = await AppEntityWithNavigationChildOneToManyRepository.GetAsync(oneToManyEntity); + + entity.AppEntityWithNavigationId = entityId2; + await AppEntityWithNavigationChildOneToManyRepository.UpdateAsync(entity); + + await uow.CompleteAsync(); + } + } + var entityUpdatedEventTriggered = false; LocalEventBus.Subscribe>(data => @@ -375,6 +420,11 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase return Task.CompletedTask; }); + LocalEventBus.Subscribe>(data => + { + throw new Exception("Should not trigger this event"); + }); + using (var scope = ServiceProvider.CreateScope()) { var uowManager = scope.ServiceProvider.GetRequiredService(); diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index d05e367ff7..8c537b2b73 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -27,6 +27,7 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Categories { get; set; } public DbSet AppEntityWithNavigations { get; set; } + public DbSet AppEntityWithNavigationChildOneToMany { get; set; } public DbSet AppEntityWithNavigationsForeign { get; set; } @@ -81,7 +82,12 @@ public class TestMigrationsDbContext : AbpDbContext b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId); b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); - b.HasOne().WithMany().HasForeignKey(x => x.AppEntityWithNavigationForeignId).IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationForeignId); }); modelBuilder.Entity(b => diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 242809bf7c..7f17346bf5 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -34,6 +34,7 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Categories { get; set; } public DbSet AppEntityWithNavigations { get; set; } + public DbSet AppEntityWithNavigationChildOneToMany { get; set; } public DbSet AppEntityWithNavigationsForeign { get; set; } @@ -107,7 +108,12 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId); b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); - b.HasOne().WithMany().HasForeignKey(x => x.AppEntityWithNavigationForeignId).IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationForeignId); }); modelBuilder.Entity(b => diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs index e7815b7622..c3ab4466a6 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs @@ -65,6 +65,17 @@ public class AppEntityWithNavigationChildOneToOneAndOneToOne : Entity public class AppEntityWithNavigationChildOneToMany : Entity { + public AppEntityWithNavigationChildOneToMany() + { + + } + + public AppEntityWithNavigationChildOneToMany(Guid id) + : base(id) + { + + } + public Guid AppEntityWithNavigationId { get; set; } public string ChildName { get; set; } @@ -107,4 +118,6 @@ public class AppEntityWithNavigationsForeign : AggregateRoot } public string Name { get; set; } + + public virtual List OneToMany { get; set; } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs index 032d714390..58010449ff 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs @@ -347,10 +347,20 @@ public abstract class AbpEntityChangeOptions_DomainEvents_Tests var entityWithNavigationForeignId = Guid.NewGuid(); var entityWithNavigationForeignId2 = Guid.NewGuid(); + var entityWithNavigationForeignId3 = Guid.NewGuid(); await AppEntityWithNavigationForeignRepository.InsertAsync(new AppEntityWithNavigationsForeign(entityWithNavigationForeignId, "TestEntityWithNavigationForeign")); await AppEntityWithNavigationForeignRepository.InsertAsync(new AppEntityWithNavigationsForeign(entityWithNavigationForeignId2, "TestEntityWithNavigationForeign2")); + await AppEntityWithNavigationForeignRepository.InsertAsync(new AppEntityWithNavigationsForeign(entityWithNavigationForeignId3, "TestEntityWithNavigationForeign3") + { + OneToMany = new List() + { + new AppEntityWithNavigations(Guid.NewGuid(), "TestEntity2"), + new AppEntityWithNavigations(Guid.NewGuid(), "TestEntity3") + } + }); var entityUpdatedEventTriggered = false; + var entityWithNavigationsForeignUpdatedEventTriggered = false; LocalEventBus.Subscribe>(data => { @@ -358,6 +368,12 @@ public abstract class AbpEntityChangeOptions_DomainEvents_Tests return Task.CompletedTask; }); + LocalEventBus.Subscribe>(data => + { + entityWithNavigationsForeignUpdatedEventTriggered = !entityWithNavigationsForeignUpdatedEventTriggered; + return Task.CompletedTask; + }); + // Test with simple property with foreign key await WithUnitOfWorkAsync(async () => { @@ -368,17 +384,39 @@ public abstract class AbpEntityChangeOptions_DomainEvents_Tests }); entityUpdatedEventTriggered.ShouldBeTrue(); - // Test only foreign key changed + // Test only foreign key change to null entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.AppEntityWithNavigationForeignId = entityWithNavigationForeignId2; + entity.AppEntityWithNavigationForeignId = null; await AppEntityWithNavigationsRepository.UpdateAsync(entity); }); entityUpdatedEventTriggered.ShouldBeFalse(); + // Test only foreign key change to new id + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithNavigationForeignId = entityWithNavigationForeignId; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test only foreign key changed + entityWithNavigationsForeignUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationForeignRepository.GetAsync(entityWithNavigationForeignId3); + entity.OneToMany.ShouldNotBeEmpty(); + entity.OneToMany.Clear(); + await AppEntityWithNavigationForeignRepository.UpdateAsync(entity); + }); + entityWithNavigationsForeignUpdatedEventTriggered.ShouldBeFalse(); + // Test with simple property with value object + entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId);