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..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;
@@ -20,6 +21,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;
@@ -53,6 +55,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);
@@ -69,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(
@@ -160,6 +166,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
List? entityChangeList = null;
if (auditLog != null)
{
+ EntityHistoryHelper.InitializeNavigationHelper(AbpEfCoreNavigationHelper);
entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList());
}
@@ -251,12 +258,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);
}
@@ -306,7 +315,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
}
}
- private void PublishEventsForTrackedEntity(EntityEntry entry)
+ protected virtual void PublishEventsForTrackedEntity(EntityEntry entry)
{
switch (entry.State)
{
@@ -314,9 +323,10 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
ApplyAbpConceptsForAddedEntity(entry);
EntityChangeEventHelper.PublishEntityCreatedEvent(entry.Entity);
break;
+
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)
{
@@ -327,13 +337,28 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext,
EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity);
}
}
-
break;
+
case EntityState.Deleted:
ApplyAbpConceptsForDeletedEntity(entry);
EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
break;
}
+
+ if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges)
+ {
+ foreach (var entityEntry in ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged && AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(x)))
+ {
+ if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted)
+ {
+ EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity);
+ }
+ else
+ {
+ EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity);
+ }
+ }
+ }
}
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