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 5dae48a105..b46402e989 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -203,6 +203,11 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, { if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) { + if (entityEntry.State == EntityState.Unchanged) + { + ApplyAbpConceptsForModifiedEntity(entityEntry, true); + } + if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) { EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); @@ -281,7 +286,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } - private void PublishEntityEvents(EntityEventReport changeReport) + protected virtual void PublishEntityEvents(EntityEventReport changeReport) { foreach (var localEvent in changeReport.DomainEvents) { @@ -395,7 +400,6 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; case EntityState.Modified: - ApplyAbpConceptsForModifiedEntity(entry); 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())) @@ -404,6 +408,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; } + ApplyAbpConceptsForModifiedEntity(entry); if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) { EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity); @@ -413,8 +418,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity); } } - else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && AbpEfCoreNavigationHelper.IsEntityEntryModified(entry)) + else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && AbpEfCoreNavigationHelper.IsNavigationEntryModified(entry)) { + ApplyAbpConceptsForModifiedEntity(entry, true); if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) { EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity); @@ -435,7 +441,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void HandlePropertiesBeforeSave() { - foreach (var entry in ChangeTracker.Entries().ToList()) + var entries = ChangeTracker.Entries().ToList(); + foreach (var entry in entries) { HandleExtraPropertiesOnSave(entry); @@ -444,6 +451,14 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, UpdateConcurrencyStamp(entry); } } + + if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) + { + foreach (var entry in AbpEfCoreNavigationHelper.GetChangedEntityEntries().Where(x => x.State == EntityState.Unchanged)) + { + UpdateConcurrencyStamp(entry); + } + } } protected virtual EntityEventReport CreateEventReport() @@ -572,9 +587,10 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, SetCreationAuditProperties(entry); } - protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry) + protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, bool forceApply = false) { - if (entry.State == EntityState.Modified && entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + if (forceApply || + entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { IncrementEntityVersionProperty(entry); SetModificationAuditProperties(entry); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs index 50a8a121d3..5a5cbe3a94 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs @@ -167,7 +167,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return EntityEntries.TryGetValue(entryId, out var abpEntityEntry) && abpEntityEntry.IsModified; } - public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, int navigationEntryIndex) + public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, int? navigationEntryIndex = null) { var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) @@ -180,7 +180,12 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return false; } - var navigationEntryProperty = abpEntityEntry.NavigationEntries.ElementAtOrDefault(navigationEntryIndex); + if (navigationEntryIndex == null) + { + return abpEntityEntry.NavigationEntries.Any(x => x.IsModified); + } + + var navigationEntryProperty = abpEntityEntry.NavigationEntries.ElementAtOrDefault(navigationEntryIndex.Value); return navigationEntryProperty != null && navigationEntryProperty.IsModified; } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/EntityChange_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/EntityChange_Tests.cs new file mode 100644 index 0000000000..d041ea7354 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Domain/EntityChange_Tests.cs @@ -0,0 +1,8 @@ +using Volo.Abp.TestApp.Testing; + +namespace Volo.Abp.EntityFrameworkCore.Domain; + +public class EntityChange_Tests : EntityChange_Tests +{ + +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/EntityChange_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/EntityChange_Tests.cs new file mode 100644 index 0000000000..79e9181f58 --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/EntityChange_Tests.cs @@ -0,0 +1,10 @@ +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.MongoDB.Domain; + +[Collection(MongoTestCollection.Name)] +public class EntityChange_Tests : EntityChange_Tests +{ + +} 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 2653948163..e7815b7622 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 @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Values; namespace Volo.Abp.TestApp.Domain; -public class AppEntityWithNavigations : AggregateRoot +public class AppEntityWithNavigations : FullAuditedAggregateRoot { protected AppEntityWithNavigations() { diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChange_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChange_Tests.cs new file mode 100644 index 0000000000..496ee78342 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChange_Tests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Modularity; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.TestApp.Testing; + +public abstract class EntityChange_Tests : TestAppTestBase + where TStartupModule : IAbpModule +{ + protected readonly IRepository AppEntityWithNavigationsRepository; + + protected EntityChange_Tests() + { + AppEntityWithNavigationsRepository = GetRequiredService>(); + } + + [Fact] + public async Task Should_Update_AbpConcepts_Properties_When_Entity_Or_Its_Navigation_Property_Changed() + { + var entityId = Guid.NewGuid(); + var entity = await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity")); + var concurrencyStamp = entity.ConcurrencyStamp; + var lastModificationTime = entity.LastModificationTime; + + // Test with simple property + await WithUnitOfWorkAsync(async () => + { + entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.Name = Guid.NewGuid().ToString(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + concurrencyStamp.ShouldNotBe(entity.ConcurrencyStamp); + lastModificationTime.ShouldNotBe(entity.LastModificationTime); + concurrencyStamp = entity.ConcurrencyStamp; + lastModificationTime = entity.LastModificationTime; + + // Test with value object + await WithUnitOfWorkAsync(async () => + { + entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + concurrencyStamp.ShouldNotBe(entity.ConcurrencyStamp); + lastModificationTime.ShouldNotBe(entity.LastModificationTime); + concurrencyStamp = entity.ConcurrencyStamp; + lastModificationTime = entity.LastModificationTime; + + // Test with one to one + await WithUnitOfWorkAsync(async () => + { + entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName" + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + concurrencyStamp.ShouldNotBe(entity.ConcurrencyStamp); + lastModificationTime.ShouldNotBe(entity.LastModificationTime); + concurrencyStamp = entity.ConcurrencyStamp; + lastModificationTime = entity.LastModificationTime; + + // Test with one to many + await WithUnitOfWorkAsync(async () => + { + entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + concurrencyStamp.ShouldNotBe(entity.ConcurrencyStamp); + lastModificationTime.ShouldNotBe(entity.LastModificationTime); + concurrencyStamp = entity.ConcurrencyStamp; + lastModificationTime = entity.LastModificationTime; + + // Test with many to many + await WithUnitOfWorkAsync(async () => + { + entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + concurrencyStamp.ShouldNotBe(entity.ConcurrencyStamp); + lastModificationTime.ShouldNotBe(entity.LastModificationTime); + + } +}