From bae8008bebd27b56a0dd02b5ab3ac6c290434e90 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:12:41 +0800 Subject: [PATCH 1/5] Publish `EntityUpdatedEvent` when navigation changes. --- .../Entities/Events/AbpEntityChangeOptions.cs | 10 + .../Abp/EntityFrameworkCore/AbpDbContext.cs | 48 +++- .../Volo/Abp/Testing/Utils/ITestCounter.cs | 2 + .../Volo/Abp/Testing/Utils/TestCounter.cs | 8 + .../Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs | 1 + .../Volo/Abp/Uow/UnitOfWorkEventRecord.cs | 7 +- .../AbpEntityFrameworkCoreTestModule.cs | 5 + .../DomainEvents/DomainEvents_Tests.cs | 4 + .../TestMigrationsDbContext.cs | 11 + .../EntityFrameworkCore/TestAppDbContext.cs | 11 + .../MemoryDb/TestAppMemoryDbContext.cs | 3 +- .../Serializer/MongoDB_DateTimeKind_Tests.cs | 7 +- .../TestApp/MongoDb/ITestAppMongoDbContext.cs | 2 + .../TestApp/MongoDb/TestAppMongoDbContext.cs | 4 +- .../Domain/AppEntityWithNavigations.cs | 67 ++++++ .../Abp/TestApp/Testing/DomainEvents_Tests.cs | 227 +++++++++++++++++- .../Distributed_User_Change_Event_Tests.cs | 3 + 17 files changed, 408 insertions(+), 12 deletions(-) create mode 100644 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs new file mode 100644 index 0000000000..0d8d74f7b2 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.Domain.Entities.Events; + +public class AbpEntityChangeOptions +{ + /// + /// Default: true. + /// Publish the EntityUpdatedEvent when any navigation property changes. + /// + public bool PublishEntityUpdatedEventWhenNavigationChanges { get; set; } = true; +} 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 05dd4527fc..c8e88d7934 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -53,6 +53,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, public IEntityChangeEventHelper EntityChangeEventHelper => LazyServiceProvider.LazyGetService(NullEntityChangeEventHelper.Instance); + public IOptions EntityChangeOptions => LazyServiceProvider.LazyGetRequiredService>(); + public IAuditPropertySetter AuditPropertySetter => LazyServiceProvider.LazyGetRequiredService(); public IEntityHistoryHelper EntityHistoryHelper => LazyServiceProvider.LazyGetService(NullEntityHistoryHelper.Instance); @@ -306,14 +308,15 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } - private void PublishEventsForTrackedEntity(EntityEntry entry) + protected virtual void PublishEventsForTrackedEntity(EntityEntry entry) { - switch (entry.State) + switch (entry.State) { case EntityState.Added: ApplyAbpConceptsForAddedEntity(entry); EntityChangeEventHelper.PublishEntityCreatedEvent(entry.Entity); break; + case EntityState.Modified: ApplyAbpConceptsForModifiedEntity(entry); if (entry.Properties.Any(x => x.IsModified && x.Metadata.ValueGenerated == ValueGenerated.Never)) @@ -327,13 +330,52 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity); } } - break; + case EntityState.Deleted: ApplyAbpConceptsForDeletedEntity(entry); EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity); break; } + + foreach (var entityEntry in new[] { entry }.Where(HasEntityEntryChanged).Concat(ChangeTracker.Entries().Where(HasEntityEntryChanged)).DistinctBy(x => x.Entity)) + { + ApplyAbpConceptsForModifiedEntity(entry); + + if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) + { + EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); + } + else + { + EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); + } + } + } + + protected virtual bool HasEntityEntryChanged(EntityEntry entry) + { + if (entry.State != EntityState.Modified && entry.State != EntityState.Unchanged) + { + return false; + } + + var changed = entry.State == EntityState.Modified && entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)); + if (changed) + { + return true; + } + + if (!changed && + EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) + { + if (entry.Navigations.Any(navigation => navigation.IsModified || (navigation is ReferenceEntry && navigation.As().TargetEntry?.State == EntityState.Modified))) + { + changed = true; + } + } + + return changed; } protected virtual void HandlePropertiesBeforeSave() diff --git a/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/ITestCounter.cs b/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/ITestCounter.cs index ec9ab4129a..0c6750f2bd 100644 --- a/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/ITestCounter.cs +++ b/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/ITestCounter.cs @@ -9,4 +9,6 @@ public interface ITestCounter int Increment(string name); int GetValue(string name); + + void ResetCount(string name); } diff --git a/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/TestCounter.cs b/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/TestCounter.cs index ae812f85f6..1a364abf81 100644 --- a/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/TestCounter.cs +++ b/framework/src/Volo.Abp.TestBase/Volo/Abp/Testing/Utils/TestCounter.cs @@ -39,4 +39,12 @@ public class TestCounter : ITestCounter, ISingletonDependency return _values.GetOrDefault(name); } } + + public void ResetCount(string name) + { + lock (_values) + { + _values[name] = 0; + } + } } diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs index 74b1b79e77..609f5501a8 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs @@ -272,6 +272,7 @@ public class UnitOfWork : IUnitOfWork, ITransientDependency } else { + eventRecord.SetOrder(eventRecords[foundIndex].EventOrder); eventRecords[foundIndex] = eventRecord; } } diff --git a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkEventRecord.cs b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkEventRecord.cs index 303f3f890c..7b55618c48 100644 --- a/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkEventRecord.cs +++ b/framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkEventRecord.cs @@ -9,7 +9,7 @@ public class UnitOfWorkEventRecord public Type EventType { get; } - public long EventOrder { get; } + public long EventOrder { get; protected set; } public bool UseOutbox { get; } @@ -29,4 +29,9 @@ public class UnitOfWorkEventRecord EventOrder = eventOrder; UseOutbox = useOutbox; } + + public void SetOrder(long order) + { + EventOrder = order; + } } 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 f23ac14fda..428e0239c3 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 @@ -48,6 +48,11 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule { opt.DefaultWithDetailsFunc = q => q.Include(p => p.Books); }); + + options.Entity(opt => + { + opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToOne).Include(p => p.OneToMany).Include(p => p.ManyToMany); + }); }); 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 da851d0a2c..cb7a5522b1 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 @@ -5,3 +5,7 @@ namespace Volo.Abp.EntityFrameworkCore.DomainEvents; public class DomainEvents_Tests : DomainEvents_Tests { } + +public class AbpEntityChangeOptions_DomainEvents_Tests : AbpEntityChangeOptions_DomainEvents_Tests +{ +} 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 2185bce2d4..702a9f34c7 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 @@ -26,6 +26,8 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Categories { get; set; } + public DbSet AppEntityWithNavigations { get; set; } + public TestMigrationsDbContext(DbContextOptions options) : base(options) { @@ -64,5 +66,14 @@ public class TestMigrationsDbContext : AbpDbContext { b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.OwnsOne(v => v.AppEntityWithValueObjectAddress); + 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(); + }); } } 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 245867776b..568c711f89 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,8 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Categories { get; set; } + public DbSet AppEntityWithNavigations { get; set; } + public TestAppDbContext(DbContextOptions options) : base(options) { @@ -92,6 +94,15 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); }); + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.OwnsOne(v => v.AppEntityWithValueObjectAddress); + 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(); + }); + modelBuilder.TryConfigureObjectExtensions(); } } diff --git a/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/TestApp/MemoryDb/TestAppMemoryDbContext.cs b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/TestApp/MemoryDb/TestAppMemoryDbContext.cs index 4336889b90..73d0059cf4 100644 --- a/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/TestApp/MemoryDb/TestAppMemoryDbContext.cs +++ b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/TestApp/MemoryDb/TestAppMemoryDbContext.cs @@ -11,7 +11,8 @@ public class TestAppMemoryDbContext : MemoryDbContext private static readonly Type[] EntityTypeList = { typeof(Person), typeof(EntityWithIntPk), - typeof(Product) + typeof(Product), + typeof(AppEntityWithNavigations) }; public override IReadOnlyList GetEntityTypes() diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Serializer/MongoDB_DateTimeKind_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Serializer/MongoDB_DateTimeKind_Tests.cs index 11277c1b74..6c1718f9a1 100644 --- a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Serializer/MongoDB_DateTimeKind_Tests.cs +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Serializer/MongoDB_DateTimeKind_Tests.cs @@ -21,9 +21,6 @@ public abstract class MongoDB_DateTimeKind_Tests : DateTimeKind_Tests Cities { get; } IMongoCollection Products { get; } + + IMongoCollection AppEntityWithNavigations { get; } } diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/TestApp/MongoDb/TestAppMongoDbContext.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/TestApp/MongoDb/TestAppMongoDbContext.cs index 9bc2606878..4d6d439c02 100644 --- a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/TestApp/MongoDb/TestAppMongoDbContext.cs +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/TestApp/MongoDb/TestAppMongoDbContext.cs @@ -24,9 +24,11 @@ public class TestAppMongoDbContext : AbpMongoDbContext, ITestAppMongoDbContext, public IMongoCollection DummyEntities => Collection(); public IMongoCollection FourthDummyEntities => Collection(); - + public IMongoCollection Products => Collection(); + public IMongoCollection AppEntityWithNavigations => Collection(); + protected internal override void CreateModel(IMongoModelBuilder modelBuilder) { base.CreateModel(modelBuilder); 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 new file mode 100644 index 0000000000..bde51a3ddb --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithNavigations.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Values; + +namespace Volo.Abp.TestApp.Domain; + +public class AppEntityWithNavigations : AggregateRoot +{ + protected AppEntityWithNavigations() + { + + } + + public AppEntityWithNavigations(Guid id, string name) + : base(id) + { + Name = name; + FullName = name; + } + + public string Name { get; set; } + + public string FullName { get; set; } + + public AppEntityWithValueObjectAddress AppEntityWithValueObjectAddress { get; set; } + + public virtual AppEntityWithNavigationChildOneToOne OneToOne { get; set; } + + public virtual List OneToMany { get; set; } + + public virtual List ManyToMany { get; set; } +} + +public class AppEntityWithValueObjectAddress : ValueObject +{ + public AppEntityWithValueObjectAddress(string country) + { + + Country = country; + } + + public string Country { get; set; } + + protected override IEnumerable GetAtomicValues() + { + yield return Country; + } +} + + +public class AppEntityWithNavigationChildOneToOne : Entity +{ + public string ChildName { get; set; } +} + +public class AppEntityWithNavigationChildOneToMany : Entity +{ + public Guid AppEntityWithNavigationId { get; set; } + + public string ChildName { get; set; } +} + +public class AppEntityWithNavigationChildManyToMany : Entity +{ + public string ChildName { 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 3bef65d544..2f94026764 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 @@ -1,6 +1,7 @@ using System; -using System.Linq; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Repositories; @@ -17,12 +18,14 @@ public abstract class DomainEvents_Tests : TestAppTestBase PersonRepository; + protected readonly IRepository AppEntityWithNavigationsRepository; protected readonly ILocalEventBus LocalEventBus; protected readonly IDistributedEventBus DistributedEventBus; protected DomainEvents_Tests() { PersonRepository = GetRequiredService>(); + AppEntityWithNavigationsRepository = GetRequiredService>(); LocalEventBus = GetRequiredService(); DistributedEventBus = GetRequiredService(); } @@ -173,6 +176,125 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => + { + entityUpdatedEventTriggered = !entityUpdatedEventTriggered; + return Task.CompletedTask; + }); + + // Test with simple property + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.Name = Guid.NewGuid().ToString(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with value object + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = null; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with one to one + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName" + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = null; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + // https://github.com/dotnet/efcore/issues/24076#issuecomment-1996623874 + // entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with one to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany.Clear(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + // https://github.com/dotnet/efcore/issues/24076#issuecomment-1996623874 + // entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with many to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany.Clear(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + } + private class MyCustomEventData { public string Value { get; set; } @@ -183,3 +305,106 @@ public abstract class DomainEvents_Tests : TestAppTestBase : TestAppTestBase + where TStartupModule : IAbpModule +{ + protected readonly IRepository AppEntityWithNavigationsRepository; + protected readonly ILocalEventBus LocalEventBus; + + protected AbpEntityChangeOptions_DomainEvents_Tests() + { + AppEntityWithNavigationsRepository = GetRequiredService>(); + LocalEventBus = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.PublishEntityUpdatedEventWhenNavigationChanges = false; + }); + + base.AfterAddApplication(services); + } + + [Fact] + public async Task Should_Not_Trigger_Domain_Events_For_Aggregate_Root_When_Navigation_Changes_Tests() + { + var entityId = Guid.NewGuid(); + await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity")); + + var entityUpdatedEventTriggered = false; + + LocalEventBus.Subscribe>(data => + { + entityUpdatedEventTriggered = !entityUpdatedEventTriggered; + return Task.CompletedTask; + }); + + // Test with simple property + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.Name = Guid.NewGuid().ToString(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with value object + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeFalse(); + + // Test with one to one + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName" + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeFalse(); + + // Test with one to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeFalse(); + + // Test with many to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeFalse(); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/Distributed_User_Change_Event_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/Distributed_User_Change_Event_Tests.cs index 3cbbcc4f8e..b7703f3d21 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/Distributed_User_Change_Event_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/Distributed_User_Change_Event_Tests.cs @@ -45,8 +45,11 @@ public class Distributed_User_Change_Event_Tests : AbpIdentityDomainTestBase [Fact] public async Task Should_Trigger_Distributed_EntityUpdated_Event() { + _testCounter.ResetCount("EntityUpdatedEto"); using (var uow = _unitOfWorkManager.Begin()) { + _testCounter.GetValue("EntityUpdatedEto").ShouldBe(0); + var user = await _userRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.NormalizeName("john.nash")); await _userManager.SetEmailAsync(user, "john.nash_UPDATED@abp.io"); From 78754133985f157ce35d862c0506846b26dbb245 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 17 Mar 2024 17:01:45 +0800 Subject: [PATCH 2/5] Detecting if a navigation has been removed. --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 38 ++++++++++++++++++- .../Abp/TestApp/Testing/DomainEvents_Tests.cs | 6 +-- 2 files changed, 39 insertions(+), 5 deletions(-) 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 c8e88d7934..236655e1a2 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; @@ -338,7 +339,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; } - foreach (var entityEntry in new[] { entry }.Where(HasEntityEntryChanged).Concat(ChangeTracker.Entries().Where(HasEntityEntryChanged)).DistinctBy(x => x.Entity)) + foreach (var entityEntry in ChangeTracker.Entries().Where(HasEntityEntryChanged)) { ApplyAbpConceptsForModifiedEntity(entry); @@ -355,6 +356,41 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual bool HasEntityEntryChanged(EntityEntry entry) { + if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && entry.State == EntityState.Unchanged) + { + var entryId = entry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1 ? entryEntity.GetKeys().FirstOrDefault()?.ToString() : null; + if (entryId != null) + { + var entity = NavigationPropertyValues.GetOrAdd(entryId, () => new Dictionary()); + var index = 0; + foreach (var navigationEntry in entry.Navigations.Where(navigation => !navigation.IsModified && navigation.IsLoaded)) + { + var navigationEntryValue = entity.GetOrAdd(index, () => navigationEntry.CurrentValue); + + if (navigationEntryValue != null && + navigationEntryValue is not ICollection && + navigationEntryValue != navigationEntry.CurrentValue) + { + return true; + } + + if (navigationEntryValue is ICollection navigationEntryValueCollection && + navigationEntry.CurrentValue is ICollection navigationEntryCurrentValueCollection && + navigationEntryValueCollection.Count != 0 && + navigationEntryCurrentValueCollection.Count == 0) + { + return true; + } + + entity[index] = navigationEntry.CurrentValue is ICollection + ? navigationEntry.CurrentValue.As().Cast().ToList() + : navigationEntry.CurrentValue; + + index++; + } + } + } + if (entry.State != EntityState.Modified && entry.State != EntityState.Unchanged) { return false; 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 2f94026764..4f82f26d95 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 @@ -239,8 +239,7 @@ public abstract class DomainEvents_Tests : TestAppTestBase : TestAppTestBase Date: Tue, 19 Mar 2024 20:27:58 +0800 Subject: [PATCH 3/5] Add `AbpEfCoreNavigationHelper`. --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 85 ++------- .../AbpEfCoreNavigationHelper.cs | 163 ++++++++++++++++ .../AbpEntityEntryNavigationProperty.cs | 20 ++ .../EntityHistory/EntityHistoryHelper.cs | 20 +- .../EntityHistory/IEntityHistoryHelper.cs | 3 + .../EntityHistory/NullEntityHistoryHelper.cs | 6 + .../Abp/Auditing/AbpAuditingTestModule.cs | 4 + .../App/Entities/AppEntityWithNavigations.cs | 13 +- .../AbpAuditingTestDbContext.cs | 3 +- .../Volo/Abp/Auditing/Auditing_Tests.cs | 177 ++++++++++++++++-- .../TestMigrationsDbContext.cs | 2 +- .../EntityFrameworkCore/TestAppDbContext.cs | 2 +- .../Domain/AppEntityWithNavigations.cs | 11 +- 13 files changed, 409 insertions(+), 100 deletions(-) create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs 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 236655e1a2..3d933435d8 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; @@ -21,6 +20,7 @@ using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.Domain.Repositories; +using Volo.Abp.EntityFrameworkCore.ChangeTrackers; using Volo.Abp.EntityFrameworkCore.EntityHistory; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.ValueConverters; @@ -246,6 +246,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, ChangeTracker.Tracked += ChangeTracker_Tracked; ChangeTracker.StateChanged += ChangeTracker_StateChanged; + EntityHistoryHelper.InitializeNavigationHelper(AbpEfCoreNavigationHelper); + if (UnitOfWorkManager is AlwaysDisableTransactionsUnitOfWorkManager) { Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; @@ -254,12 +256,14 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e) { + AbpEfCoreNavigationHelper.ChangeTracker_Tracked(ChangeTracker, sender, e); FillExtraPropertiesForTrackedEntities(e); PublishEventsForTrackedEntity(e.Entry); } protected virtual void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e) { + AbpEfCoreNavigationHelper.ChangeTracker_StateChanged(ChangeTracker, sender, e); PublishEventsForTrackedEntity(e.Entry); } @@ -311,7 +315,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void PublishEventsForTrackedEntity(EntityEntry entry) { - switch (entry.State) + switch (entry.State) { case EntityState.Added: ApplyAbpConceptsForAddedEntity(entry); @@ -320,7 +324,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, case EntityState.Modified: ApplyAbpConceptsForModifiedEntity(entry); - if (entry.Properties.Any(x => x.IsModified && x.Metadata.ValueGenerated == ValueGenerated.Never)) + if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) { @@ -339,79 +343,20 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; } - foreach (var entityEntry in ChangeTracker.Entries().Where(HasEntityEntryChanged)) - { - ApplyAbpConceptsForModifiedEntity(entry); - - if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) - { - EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); - } - else - { - EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); - } - } - } - - protected virtual bool HasEntityEntryChanged(EntityEntry entry) - { - if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && entry.State == EntityState.Unchanged) + if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) { - var entryId = entry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1 ? entryEntity.GetKeys().FirstOrDefault()?.ToString() : null; - if (entryId != null) + foreach (var entityEntry in ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged && AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(x))) { - var entity = NavigationPropertyValues.GetOrAdd(entryId, () => new Dictionary()); - var index = 0; - foreach (var navigationEntry in entry.Navigations.Where(navigation => !navigation.IsModified && navigation.IsLoaded)) + if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) { - var navigationEntryValue = entity.GetOrAdd(index, () => navigationEntry.CurrentValue); - - if (navigationEntryValue != null && - navigationEntryValue is not ICollection && - navigationEntryValue != navigationEntry.CurrentValue) - { - return true; - } - - if (navigationEntryValue is ICollection navigationEntryValueCollection && - navigationEntry.CurrentValue is ICollection navigationEntryCurrentValueCollection && - navigationEntryValueCollection.Count != 0 && - navigationEntryCurrentValueCollection.Count == 0) - { - return true; - } - - entity[index] = navigationEntry.CurrentValue is ICollection - ? navigationEntry.CurrentValue.As().Cast().ToList() - : navigationEntry.CurrentValue; - - index++; + EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); + } + else + { + EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); } } } - - if (entry.State != EntityState.Modified && entry.State != EntityState.Unchanged) - { - return false; - } - - var changed = entry.State == EntityState.Modified && entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd)); - if (changed) - { - return true; - } - - if (!changed && - EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) - { - if (entry.Navigations.Any(navigation => navigation.IsModified || (navigation is ReferenceEntry && navigation.As().TargetEntry?.State == EntityState.Modified))) - { - changed = true; - } - } - - return changed; } protected virtual void HandlePropertiesBeforeSave() 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 new file mode 100644 index 0000000000..74f0831be2 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; + +/// +/// Refactor this class after EF Core supports this case. +/// https://github.com/dotnet/efcore/issues/24076#issuecomment-1996623874 +/// +public class AbpEfCoreNavigationHelper : ITransientDependency +{ + private Dictionary> EntityEntryNavigationProperties { get; } = new (); + + public virtual void ChangeTracker_Tracked(ChangeTracker changeTracker, object? sender, EntityTrackedEventArgs e) + { + foreach (var entry in changeTracker.Entries()) + { + EntityEntryTrackedOrStateChanged(entry); + } + } + + public virtual void ChangeTracker_StateChanged(ChangeTracker changeTracker, object? sender, EntityStateChangedEventArgs e) + { + foreach (var entry in changeTracker.Entries()) + { + EntityEntryTrackedOrStateChanged(entry); + } + } + + private void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry) + { + if (entityEntry.State != EntityState.Unchanged) + { + return; + } + + var entryId = GetEntityId(entityEntry); + if (entryId == null) + { + return; + } + + var navigationProperties = EntityEntryNavigationProperties.GetOrAdd(entryId, () => new List()); + var index = 0; + foreach (var navigationEntry in entityEntry.Navigations.Where(navigation => !navigation.IsModified)) + { + if (!navigationEntry.IsLoaded) + { + index++; + continue; + } + + var currentValue = navigationEntry.CurrentValue; + if (navigationEntry.CurrentValue is ICollection collection) + { + currentValue = collection.Cast().ToList(); + } + + var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); + if (navigationProperty != null) + { + if (!navigationProperty.IsChanged && (navigationProperty.Value == null || IsCollectionAndEmpty(navigationProperty.Value))) + { + navigationProperty.Value = currentValue; + navigationProperty.IsChanged = currentValue != null && !IsCollectionAndEmpty(currentValue); + } + + if (!navigationProperty.IsChanged && navigationProperty.Value != null && !IsCollectionAndEmpty(navigationProperty.Value)) + { + navigationProperty.Value = currentValue; + navigationProperty.IsChanged = currentValue == null || IsCollectionAndEmpty(currentValue); + } + } + else + { + navigationProperties.Add(new AbpEntityEntryNavigationProperty(index, navigationEntry.Metadata.Name, currentValue, false)); + } + + index++; + } + } + + public bool IsEntityEntryNavigationChanged(EntityEntry entityEntry) + { + if (entityEntry.State == EntityState.Modified) + { + return true; + } + + var entryId = GetEntityId(entityEntry); + if (entryId == null) + { + return false; + } + + var index = 0; + foreach (var navigationEntry in entityEntry.Navigations) + { + if (navigationEntry.IsModified || (navigationEntry is ReferenceEntry && navigationEntry.As().TargetEntry?.State == EntityState.Modified)) + { + return true; + } + + EntityEntryTrackedOrStateChanged(entityEntry); + + if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties)) + { + var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); + if (navigationProperty != null && navigationProperty.IsChanged) + { + return true; + } + } + + index++; + } + + return false; + } + + public bool IsEntityEntryNavigationChanged(NavigationEntry navigationEntry, int index) + { + if (navigationEntry.IsModified || (navigationEntry is ReferenceEntry && navigationEntry.As().TargetEntry?.State == EntityState.Modified)) + { + return true; + } + + var entryId = GetEntityId(navigationEntry.EntityEntry); + if (entryId == null) + { + return false; + } + + if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties)) + { + var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); + if (navigationProperty != null && navigationProperty.IsChanged) + { + return true; + } + } + + return false; + } + + private string? GetEntityId(EntityEntry entityEntry) + { + return entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1 + ? entryEntity.GetKeys().FirstOrDefault()?.ToString() + : null; + } + + private bool IsCollectionAndEmpty(object? value) + { + return value is ICollection && value is ICollection collection && collection.Count == 0; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs new file mode 100644 index 0000000000..c9a4ae2ad8 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs @@ -0,0 +1,20 @@ +namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; + +public class AbpEntityEntryNavigationProperty +{ + public int Index { get; set; } + + public string Name { get; set; } + + public object? Value { get; set; } + + public bool IsChanged { get; set; } + + public AbpEntityEntryNavigationProperty(int index, string name, object? value, bool isChanged) + { + Index = index; + Name = name; + Value = value; + IsChanged = isChanged; + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index 5229e9aab6..54d917e76b 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.Extensions.Logging; @@ -13,6 +12,7 @@ using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Values; +using Volo.Abp.EntityFrameworkCore.ChangeTrackers; using Volo.Abp.Json; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; @@ -30,6 +30,8 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency protected IAuditingHelper AuditingHelper { get; } protected IClock Clock { get; } + protected AbpEfCoreNavigationHelper? AbpEfCoreNavigationHelper { get; set; } + public EntityHistoryHelper( IAuditingStore auditingStore, IOptions options, @@ -46,6 +48,11 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency Logger = NullLogger.Instance; } + public void InitializeNavigationHelper(AbpEfCoreNavigationHelper abpEfCoreNavigationHelper) + { + AbpEfCoreNavigationHelper = abpEfCoreNavigationHelper; + } + public virtual List CreateChangeList(ICollection entityEntries) { var list = new List(); @@ -186,11 +193,12 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } - if (Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged) + if (entityEntry.State == EntityState.Unchanged && Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null) { + var index = 0; foreach (var navigation in entityEntry.Navigations) { - if (navigation.IsModified || (navigation is ReferenceEntry && navigation.As().TargetEntry?.State == EntityState.Modified)) + if (navigation.IsModified || AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(navigation, index)) { propertyChanges.Add(new EntityPropertyChangeInfo { @@ -198,6 +206,8 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency PropertyTypeFullName = navigation.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! }); } + + index++; } } @@ -245,9 +255,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency protected virtual bool HasNavigationPropertiesChanged(EntityEntry entityEntry) { - return Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged && - (entityEntry.Navigations.Any(navigationEntry => navigationEntry.IsModified) || - entityEntry.Navigations.Where(x => x is ReferenceEntry).Cast().Any(x => x.TargetEntry != null && x.TargetEntry.State == EntityState.Modified)); + return entityEntry.State == EntityState.Unchanged && Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null && AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(entityEntry); } protected virtual bool ShouldSavePropertyHistory(PropertyEntry propertyEntry, bool defaultValue) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs index 2a860ee9cf..ee8d9aa528 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/IEntityHistoryHelper.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking; using Volo.Abp.Auditing; +using Volo.Abp.EntityFrameworkCore.ChangeTrackers; namespace Volo.Abp.EntityFrameworkCore.EntityHistory; public interface IEntityHistoryHelper { + void InitializeNavigationHelper(AbpEfCoreNavigationHelper abpEfCoreNavigationHelper); + List CreateChangeList(ICollection entityEntries); void UpdateChangeList(List entityChanges); diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs index 285b52e942..6e53848d8f 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/NullEntityHistoryHelper.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Microsoft.EntityFrameworkCore.ChangeTracking; using Volo.Abp.Auditing; +using Volo.Abp.EntityFrameworkCore.ChangeTrackers; namespace Volo.Abp.EntityFrameworkCore.EntityHistory; @@ -13,6 +14,11 @@ public class NullEntityHistoryHelper : IEntityHistoryHelper } + public void InitializeNavigationHelper(AbpEfCoreNavigationHelper abpEfCoreNavigationHelper) + { + + } + public List CreateChangeList(ICollection entityEntries) { return new List(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs index 3a21cfaf5a..65c9d0fa57 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs @@ -24,6 +24,10 @@ public class AbpAuditingTestModule : AbpModule context.Services.AddAbpDbContext(options => { options.AddDefaultRepositories(true); + options.Entity(opt => + { + opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToOne).Include(p => p.OneToMany).Include(p => p.ManyToMany); + }); }); var sqliteConnection = CreateDatabaseAndGetConnection(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs index e68d9d94b8..e13523dabe 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs @@ -23,6 +23,8 @@ public class AppEntityWithNavigations : AggregateRoot public string FullName { get; set; } + public AppEntityWithValueObjectAddress AppEntityWithValueObjectAddress { get; set; } + public virtual AppEntityWithNavigationChildOneToOne OneToOne { get; set; } public virtual List OneToMany { get; set; } @@ -45,7 +47,16 @@ public class AppEntityWithNavigationChildOneToMany : Entity } [Audited] -public class AppEntityWithNavigationChildManyToMany : Entity +public class AppEntityWithNavigationChildManyToMany : AggregateRoot { public string ChildName { get; set; } + + public virtual List ManyToMany { get; set; } +} + +public class AppEntityWithNavigationsAndAppEntityWithNavigationChildManyToMany +{ + public Guid AppEntityWithNavigationsId { get; set; } + + public Guid AppEntityWithNavigationChildManyToManyId { get; set; } } 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 f6d4bf49ef..b102396cb0 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 @@ -48,9 +48,10 @@ public class AbpAuditingTestDbContext : AbpDbContext modelBuilder.Entity(b => { b.ConfigureByConvention(); + b.OwnsOne(x => x.AppEntityWithValueObjectAddress); 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(); + b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); } diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs index a5060dc595..954e6222ee 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs @@ -385,10 +385,10 @@ public class Auditing_Tests : AbpAuditingTestBase #pragma warning disable 4014 AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 3 && - x.EntityChanges[0].ChangeType == EntityChangeType.Created && - x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && - x.EntityChanges[1].ChangeType == EntityChangeType.Updated && - x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Created && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && x.EntityChanges[2].ChangeType == EntityChangeType.Deleted && x.EntityChanges[2].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName)); AuditingStore.ClearReceivedCalls(); @@ -411,16 +411,16 @@ public class Auditing_Tests : AbpAuditingTestBase #pragma warning disable 4014 AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && x.EntityChanges[0].ChangeType == EntityChangeType.Updated && - x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && x.EntityChanges[0].PropertyChanges.Count == 1 && - x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && - x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" && - x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"" && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObject.AppEntityWithValueObjectAddress) && x.EntityChanges[1].ChangeType == EntityChangeType.Updated && - x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && x.EntityChanges[1].PropertyChanges.Count == 1 && - x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObject.AppEntityWithValueObjectAddress))); + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && + x.EntityChanges[1].PropertyChanges[0].OriginalValue == "\"England\"" && + x.EntityChanges[1].PropertyChanges[0].NewValue == "\"Germany\"")); AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 @@ -439,11 +439,16 @@ public class Auditing_Tests : AbpAuditingTestBase } #pragma warning disable 4014 - AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && - x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && - x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && - x.EntityChanges[0].PropertyChanges.Count == 2 && - x.EntityChanges[0].PropertyChanges.All(p => p.NewValue == null))); + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObject.AppEntityWithValueObjectAddress) && + + x.EntityChanges[1].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[1].PropertyChanges.Count == 2 && + x.EntityChanges[1].PropertyChanges.All(p => p.NewValue == null))); #pragma warning restore 4014 } @@ -511,6 +516,33 @@ public class Auditing_Tests : AbpAuditingTestBase AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToOne = null; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[1].PropertyChanges.Count == 1 && + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToOne) && + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) @@ -544,6 +576,32 @@ public class Auditing_Tests : AbpAuditingTestBase AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToMany = null; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[1].PropertyChanges.Count == 1 && + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) && + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) @@ -574,6 +632,36 @@ public class Auditing_Tests : AbpAuditingTestBase x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) && x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.ManyToMany.Clear(); + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName && + + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigationChildManyToMany).FullName && + x.EntityChanges[1].PropertyChanges.Count == 1 && + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildManyToMany.ManyToMany) && + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); + #pragma warning restore 4014 } } @@ -648,10 +736,10 @@ public class Auditing_SaveEntityHistoryWhenNavigationChanges_Tests : AbpAuditing #pragma warning disable 4014 AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 3 && - x.EntityChanges[0].ChangeType == EntityChangeType.Created && - x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && - x.EntityChanges[1].ChangeType == EntityChangeType.Updated && - x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Created && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && x.EntityChanges[2].ChangeType == EntityChangeType.Deleted && x.EntityChanges[2].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName)); AuditingStore.ClearReceivedCalls(); @@ -767,6 +855,31 @@ public class Auditing_SaveEntityHistoryWhenNavigationChanges_Tests : AbpAuditing AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToOne = null; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildOneToOne.ChildName) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) @@ -800,6 +913,32 @@ public class Auditing_SaveEntityHistoryWhenNavigationChanges_Tests : AbpAuditing AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToMany.Clear(); + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName && + x.EntityChanges[0].PropertyChanges.Count == 2 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildOneToMany.AppEntityWithNavigationId) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(Guid).FullName && + x.EntityChanges[0].PropertyChanges[1].PropertyName == nameof(AppEntityWithNavigationChildOneToMany.ChildName) && + x.EntityChanges[0].PropertyChanges[1].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) 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 702a9f34c7..5de4bd6369 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 @@ -73,7 +73,7 @@ public class TestMigrationsDbContext : AbpDbContext b.OwnsOne(v => v.AppEntityWithValueObjectAddress); 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(); + b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); } } 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 568c711f89..63b57ec147 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 @@ -100,7 +100,7 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.OwnsOne(v => v.AppEntityWithValueObjectAddress); 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(); + b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); modelBuilder.TryConfigureObjectExtensions(); 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 bde51a3ddb..b0c2e4f7bf 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 @@ -61,7 +61,16 @@ public class AppEntityWithNavigationChildOneToMany : Entity public string ChildName { get; set; } } -public class AppEntityWithNavigationChildManyToMany : Entity +public class AppEntityWithNavigationChildManyToMany : AggregateRoot { public string ChildName { get; set; } + + public virtual List ManyToMany { get; set; } +} + +public class AppEntityWithNavigationsAndAppEntityWithNavigationChildManyToMany +{ + public Guid AppEntityWithNavigationsId { get; set; } + + public Guid AppEntityWithNavigationChildManyToManyId { get; set; } } From 0eae923e176f6034337145db8f2a3b732c3fe833 Mon Sep 17 00:00:00 2001 From: maliming <6908465+maliming@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:54:10 +0800 Subject: [PATCH 4/5] Update AbpDbContext.cs --- .../Volo/Abp/EntityFrameworkCore/AbpDbContext.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 3d933435d8..b639324ea9 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -163,6 +163,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, List? entityChangeList = null; if (auditLog != null) { + EntityHistoryHelper.InitializeNavigationHelper(AbpEfCoreNavigationHelper); entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList()); } @@ -246,8 +247,6 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, ChangeTracker.Tracked += ChangeTracker_Tracked; ChangeTracker.StateChanged += ChangeTracker_StateChanged; - EntityHistoryHelper.InitializeNavigationHelper(AbpEfCoreNavigationHelper); - if (UnitOfWorkManager is AlwaysDisableTransactionsUnitOfWorkManager) { Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; From 56c8d4211b6e6e790d4f71ab276277b9cee8bd88 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 25 Mar 2024 17:48:44 +0800 Subject: [PATCH 5/5] Update AbpDbContext.cs --- .../Volo/Abp/EntityFrameworkCore/AbpDbContext.cs | 3 +++ 1 file changed, 3 insertions(+) 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 b639324ea9..65958ab5e5 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Auditing; @@ -72,6 +73,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, public ILogger> Logger => LazyServiceProvider.LazyGetService>>(NullLogger>.Instance); + public AbpEfCoreNavigationHelper AbpEfCoreNavigationHelper => LazyServiceProvider.LazyGetRequiredService(); + private static readonly MethodInfo ConfigureBasePropertiesMethodInfo = typeof(AbpDbContext) .GetMethod(