From 5d705b15eaca23ea7534546e6517021cde27c0d6 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 17 Apr 2024 18:16:31 +0800 Subject: [PATCH 1/6] Improve the performance of `AbpEfCoreNavigationHelper`. --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 33 ++-- .../AbpEfCoreNavigationHelper.cs | 172 +++++++++--------- .../ChangeTrackers/AbpEntityEntry.cs | 49 +++++ .../AbpEntityEntryNavigationProperty.cs | 28 --- .../EntityHistory/EntityHistoryHelper.cs | 15 +- .../Abp/TestApp/Testing/DomainEvents_Tests.cs | 50 ++++- 6 files changed, 201 insertions(+), 146 deletions(-) create mode 100644 framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs delete 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 09c1f0a3d7..f350d0c614 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -162,6 +162,21 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, { try { + if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) + { + foreach (var entityEntry in AbpEfCoreNavigationHelper.GetChangedEntityEntries()) + { + if (entityEntry.Entity is ISoftDelete && entityEntry.Entity.As().IsDeleted) + { + EntityChangeEventHelper.PublishEntityDeletedEvent(entityEntry.Entity); + } + else + { + EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); + } + } + } + var auditLog = AuditingManager?.Current?.Log; List? entityChangeList = null; if (auditLog != null) @@ -344,8 +359,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity); } } - else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && - (entry.Navigations.Any(x => x.IsModified) || AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(entry))) + else if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && AbpEfCoreNavigationHelper.IsEntityEntryModified(entry)) { if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) { @@ -363,21 +377,6 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity); break; } - - if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) - { - foreach (var entityEntry in AbpEfCoreNavigationHelper.GetChangedEntityEntries()) - { - 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 index d145ed5316..bdea524940 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 @@ -4,6 +4,9 @@ using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; @@ -15,18 +18,18 @@ namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; /// public class AbpEfCoreNavigationHelper : ITransientDependency { - protected Dictionary> EntityEntryNavigationProperties { get; } = new Dictionary>(); + protected Dictionary EntityEntries { get; } = new(); public virtual void ChangeTracker_Tracked(ChangeTracker changeTracker, object? sender, EntityTrackedEventArgs e) { EntityEntryTrackedOrStateChanged(e.Entry); - DetectChanges(); + DetectChanges(changeTracker, e.Entry); } public virtual void ChangeTracker_StateChanged(ChangeTracker changeTracker, object? sender, EntityStateChangedEventArgs e) { EntityEntryTrackedOrStateChanged(e.Entry); - DetectChanges(); + DetectChanges(changeTracker, e.Entry); } protected virtual void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry) @@ -42,83 +45,103 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return; } - var navigationProperties = EntityEntryNavigationProperties.GetOrAdd(entryId, () => new List()); - var index = 0; - foreach (var navigationEntry in entityEntry.Navigations.Where(navigation => !navigation.IsModified)) + if (EntityEntries.ContainsKey(entryId)) { - var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); - if (navigationProperty != null) + return; + } + + EntityEntries.Add(entryId, new AbpEntityEntry(entryId, entityEntry)); + } + + protected virtual void DetectChanges(ChangeTracker changeTracker, EntityEntry entityEntry) + { + if (entityEntry.State != EntityState.Added && + entityEntry.State != EntityState.Deleted && + entityEntry.State != EntityState.Modified) + { + return; + } + +#pragma warning disable EF1001 + var stateManager = changeTracker.Context.GetDependencies().StateManager; + var internalEntityEntityEntry = stateManager.Entries.FirstOrDefault(x => x.Entity == entityEntry.Entity); + if (internalEntityEntityEntry == null) + { + return; + } + + var foreignKeys = entityEntry.Metadata.GetForeignKeys().ToList(); + foreach (var foreignKey in foreignKeys) + { + var principal = stateManager.FindPrincipal(internalEntityEntityEntry, foreignKey); + if (principal == null) { - if (navigationProperty.Value == null || IsCollectionAndEmpty(navigationProperty.Value)) - { - navigationProperty.Value = navigationEntry.CurrentValue; - } + continue; } - else + + var entryId = GetEntityId(principal.ToEntityEntry()); + if (entryId == null || !EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) { - navigationProperties.Add(new AbpEntityEntryNavigationProperty(index, navigationEntry.Metadata.Name, navigationEntry.CurrentValue, false, entityEntry, navigationEntry)); + continue; } - index++; + abpEntityEntry.IsModified = true; + var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == foreignKey) ?? + abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == foreignKey); + if (navigationEntry != null) + { + navigationEntry.IsModified = true; + } } - } - protected virtual void DetectChanges() - { - foreach (var entityEntryNavigationProperties in EntityEntryNavigationProperties) + var skipNavigations = entityEntry.Metadata.GetSkipNavigations().ToList(); + foreach (var skipNavigation in skipNavigations) { - foreach (var navigationProperty in entityEntryNavigationProperties.Value.Where(x => !x.IsChanged && x.EntityEntry.State == EntityState.Unchanged)) + var joinEntityType = skipNavigation.JoinEntityType; + var foreignKey = skipNavigation.ForeignKey; + var inverseForeignKey = skipNavigation.Inverse.ForeignKey; + foreach (var joinEntry in stateManager.Entries) { - if (navigationProperty.NavigationEntry.IsModified) + if (joinEntry.EntityType != joinEntityType || stateManager.FindPrincipal(joinEntry, foreignKey) != internalEntityEntityEntry) + { + continue; + } + + var principal = stateManager.FindPrincipal(joinEntry, inverseForeignKey); + if (principal == null) { - navigationProperty.IsChanged = true; continue; } - if (navigationProperty.Value == null || IsCollectionAndEmpty(navigationProperty.Value)) + var entryId = GetEntityId(principal.ToEntityEntry()); + if (entryId == null || !EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) { - if (navigationProperty.NavigationEntry.CurrentValue != null || IsCollectionAndNotEmpty(navigationProperty.NavigationEntry.CurrentValue)) - { - if (navigationProperty.NavigationEntry.CurrentValue is ICollection collection) - { - navigationProperty.Value = collection.Cast().ToList(); - } - else - { - navigationProperty.Value = navigationProperty.NavigationEntry.CurrentValue; - } - } + continue; } - if (navigationProperty.Value != null || IsCollectionAndNotEmpty(navigationProperty.Value)) + abpEntityEntry.IsModified = true; + abpEntityEntry.IsModified = true; + var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == inverseForeignKey) ?? + abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == inverseForeignKey); + if (navigationEntry != null) { - if (navigationProperty.NavigationEntry.CurrentValue == null || IsCollectionAndEmpty(navigationProperty.NavigationEntry.CurrentValue)) - { - if (IsCollectionAndEmpty(navigationProperty.Value) && IsCollectionAndEmpty(navigationProperty.NavigationEntry.CurrentValue)) - { - continue; - } - - navigationProperty.IsChanged = true; - } + navigationEntry.IsModified = true; } } } +#pragma warning restore EF1001 } public virtual List GetChangedEntityEntries() { - DetectChanges(); - return EntityEntryNavigationProperties - .SelectMany(x => x.Value) - .Where(x => x.NavigationEntry.IsModified || x.IsChanged) - .Select(x => x.EntityEntry) + return EntityEntries + .Where(x => x.Value.IsModified) + .Select(x => x.Value.EntityEntry) .ToList(); } - public virtual bool IsEntityEntryNavigationChanged(EntityEntry entityEntry) + public virtual bool IsEntityEntryModified(EntityEntry entityEntry) { - DetectChanges(); if (entityEntry.State == EntityState.Modified) { return true; @@ -130,62 +153,35 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return false; } - if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties)) - { - return navigationProperties.Any(x => x.IsChanged) || - navigationProperties.Any(x => x.NavigationEntry.IsModified) || - navigationProperties.Any(x => - x.NavigationEntry is ReferenceEntry && - x.NavigationEntry.As().TargetEntry?.State == EntityState.Modified); - } - - return false; + return EntityEntries.TryGetValue(entryId, out var abpEntityEntry) && abpEntityEntry.IsModified; } - public virtual bool IsEntityEntryNavigationChanged(NavigationEntry navigationEntry, int index) + public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, NavigationEntry navigationEntry, int index) { - if (navigationEntry.IsModified || (navigationEntry is ReferenceEntry && navigationEntry.As().TargetEntry?.State == EntityState.Modified)) - { - return true; - } - - var entryId = GetEntityId(navigationEntry.EntityEntry); + var entryId = GetEntityId(entityEntry); if (entryId == null) { return false; } - if (EntityEntryNavigationProperties.TryGetValue(entryId, out var navigationProperties)) + if (!EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) { - var navigationProperty = navigationProperties.FirstOrDefault(x => x.Index == index); - if (navigationProperty != null && navigationProperty.IsChanged) - { - return true; - } + return false; } - return false; + var navigationEntryProperty = abpEntityEntry.NavigationEntries.ElementAtOrDefault(index); + return navigationEntryProperty != null && navigationEntryProperty.IsModified; } - public void Clear() - { - EntityEntryNavigationProperties.Clear(); - } - - private string? GetEntityId(EntityEntry entityEntry) + protected virtual 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; - } - - private bool IsCollectionAndNotEmpty(object? value) + public void Clear() { - return value is ICollection && value is ICollection collection && collection.Count != 0; + EntityEntries.Clear(); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs new file mode 100644 index 0000000000..c26ae2bbf1 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; + +public class AbpEntityEntry +{ + public string Id { get; set; } + + public EntityEntry EntityEntry { get; set; } + + public List NavigationEntries { get; set; } + + private bool _isModified; + public bool IsModified + { + get + { + return _isModified || NavigationEntries.Any(n => n.IsModified); + } + set + { + _isModified = value; + } + } + + public AbpEntityEntry(string id, EntityEntry entityEntry) + { + Id = id; + EntityEntry = entityEntry; + NavigationEntries = EntityEntry.Navigations.Select(x => new AbpNavigationEntry(x, x.Metadata.Name)).ToList(); + } +} + +public class AbpNavigationEntry +{ + public NavigationEntry NavigationEntry { get; set; } + + public string Name { get; set; } + + public bool IsModified { get; set; } + + public AbpNavigationEntry(NavigationEntry navigationEntry, string name) + { + NavigationEntry = navigationEntry; + Name = name; + } +} 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 deleted file mode 100644 index a3cec7cf64..0000000000 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntryNavigationProperty.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore.ChangeTracking; - -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 EntityEntry EntityEntry { get; set; } - - public NavigationEntry NavigationEntry { get; set; } - - public AbpEntityEntryNavigationProperty(int index, string name, object? value, bool isChanged, EntityEntry entityEntry, NavigationEntry navigationEntry) - { - Index = index; - Name = name; - Value = value; - IsChanged = isChanged; - EntityEntry = entityEntry; - NavigationEntry = navigationEntry; - } -} 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 ef6bd230f5..49a25601a4 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 @@ -193,17 +193,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } - if (entityEntry.State == EntityState.Unchanged && Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null) + if (Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null) { var index = 0; - foreach (var navigation in entityEntry.Navigations) + foreach (var navigationEntry in entityEntry.Navigations) { - if (navigation.IsModified || AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(navigation, index)) + if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, navigationEntry, index)) { propertyChanges.Add(new EntityPropertyChangeInfo { - PropertyName = navigation.Metadata.Name, - PropertyTypeFullName = navigation.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! + PropertyName = navigationEntry.Metadata.Name, + PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! }); } @@ -255,10 +255,9 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency protected virtual bool HasNavigationPropertiesChanged(EntityEntry entityEntry) { - return entityEntry.State == EntityState.Unchanged && - Options.SaveEntityHistoryWhenNavigationChanges && + return Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null && - AbpEfCoreNavigationHelper.IsEntityEntryNavigationChanged(entityEntry); + AbpEfCoreNavigationHelper.IsEntityEntryModified(entityEntry); } protected virtual bool ShouldSavePropertyHistory(PropertyEntry propertyEntry, bool defaultValue) 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 74f8198aa7..ddb91e7ba6 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,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -209,22 +209,32 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => + // Test with value object + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => { - throw new Exception("Should not trigger this event"); + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - // Test with value object entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + entity.AppEntityWithValueObjectAddress.Country = "USA"; await AppEntityWithNavigationsRepository.UpdateAsync(entity); }); entityUpdatedEventTriggered.ShouldBeTrue(); personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + LocalEventBus.Subscribe>(data => + { + throw new Exception("Should not trigger this event"); + }); + entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { @@ -249,6 +259,16 @@ public abstract class DomainEvents_Tests : TestAppTestBase + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne.ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + LocalEventBus.Subscribe>(data => { throw new Exception("Should not trigger this event"); @@ -282,6 +302,16 @@ public abstract class DomainEvents_Tests : TestAppTestBase + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany[0].ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + LocalEventBus.Subscribe>(data => { throw new Exception("Should not trigger this event"); @@ -314,6 +344,16 @@ public abstract class DomainEvents_Tests : TestAppTestBase + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany[0].ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { From 34f86e67d0e733f077e71d70086d55c27395a52c Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 17 Apr 2024 19:59:35 +0800 Subject: [PATCH 2/6] Remove unnecessary `NavigationEntry` parameter. --- .../ChangeTrackers/AbpEfCoreNavigationHelper.cs | 4 ++-- .../EntityHistory/EntityHistoryHelper.cs | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) 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 bdea524940..5ee3ce7aaf 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 @@ -156,7 +156,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return EntityEntries.TryGetValue(entryId, out var abpEntityEntry) && abpEntityEntry.IsModified; } - public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, NavigationEntry navigationEntry, int index) + public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, int navigationEntryIndex) { var entryId = GetEntityId(entityEntry); if (entryId == null) @@ -169,7 +169,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return false; } - var navigationEntryProperty = abpEntityEntry.NavigationEntries.ElementAtOrDefault(index); + var navigationEntryProperty = abpEntityEntry.NavigationEntries.ElementAtOrDefault(navigationEntryIndex); return navigationEntryProperty != null && navigationEntryProperty.IsModified; } 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 49a25601a4..a52ac2f437 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 @@ -195,10 +195,9 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency if (Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null) { - var index = 0; - foreach (var navigationEntry in entityEntry.Navigations) + foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) { - if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, navigationEntry, index)) + if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { propertyChanges.Add(new EntityPropertyChangeInfo { @@ -206,8 +205,6 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! }); } - - index++; } } From 474037eaf516314df795a70ca19d7d65fdf196f1 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 17 Apr 2024 20:09:40 +0800 Subject: [PATCH 3/6] Update AbpEfCoreNavigationHelper.cs --- .../ChangeTrackers/AbpEfCoreNavigationHelper.cs | 1 - 1 file changed, 1 deletion(-) 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 5ee3ce7aaf..6ee01fdd8e 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 @@ -119,7 +119,6 @@ public class AbpEfCoreNavigationHelper : ITransientDependency continue; } - abpEntityEntry.IsModified = true; abpEntityEntry.IsModified = true; var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == inverseForeignKey) ?? abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == inverseForeignKey); From 2604c817d58117358c01944d6a7ca7c078a1f782 Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 20 Apr 2024 11:11:13 +0800 Subject: [PATCH 4/6] Support `RecursiveDetectChanges`. --- .../Abp/EntityFrameworkCore/AbpDbContext.cs | 4 +-- .../AbpEfCoreNavigationHelper.cs | 36 ++++++++++++------- .../ChangeTrackers/AbpEntityEntry.cs | 3 +- .../AbpEntityFrameworkCoreTestModule.cs | 2 +- .../TestMigrationsDbContext.cs | 14 +++++++- .../EntityFrameworkCore/TestAppDbContext.cs | 12 +++++++ .../Domain/AppEntityWithNavigations.cs | 18 ++++++++-- .../Abp/TestApp/Testing/DomainEvents_Tests.cs | 35 ++++++++++++++++-- 8 files changed, 103 insertions(+), 21 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 f350d0c614..41b39ddea9 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -274,14 +274,14 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e) { - AbpEfCoreNavigationHelper.ChangeTracker_Tracked(ChangeTracker, sender, e); + AbpEfCoreNavigationHelper.ChangeTracker_Tracked(sender, e); FillExtraPropertiesForTrackedEntities(e); PublishEventsForTrackedEntity(e.Entry); } protected virtual void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e) { - AbpEfCoreNavigationHelper.ChangeTracker_StateChanged(ChangeTracker, sender, e); + AbpEfCoreNavigationHelper.ChangeTracker_StateChanged(sender, e); PublishEventsForTrackedEntity(e.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 6ee01fdd8e..4a57f0803c 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 @@ -1,12 +1,9 @@ -using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Entities; @@ -20,16 +17,16 @@ public class AbpEfCoreNavigationHelper : ITransientDependency { protected Dictionary EntityEntries { get; } = new(); - public virtual void ChangeTracker_Tracked(ChangeTracker changeTracker, object? sender, EntityTrackedEventArgs e) + public virtual void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e) { EntityEntryTrackedOrStateChanged(e.Entry); - DetectChanges(changeTracker, e.Entry); + DetectChanges(e.Entry); } - public virtual void ChangeTracker_StateChanged(ChangeTracker changeTracker, object? sender, EntityStateChangedEventArgs e) + public virtual void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e) { EntityEntryTrackedOrStateChanged(e.Entry); - DetectChanges(changeTracker, e.Entry); + DetectChanges(e.Entry); } protected virtual void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry) @@ -53,7 +50,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency EntityEntries.Add(entryId, new AbpEntityEntry(entryId, entityEntry)); } - protected virtual void DetectChanges(ChangeTracker changeTracker, EntityEntry entityEntry) + protected virtual void DetectChanges(EntityEntry entityEntry) { if (entityEntry.State != EntityState.Added && entityEntry.State != EntityState.Deleted && @@ -62,8 +59,13 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return; } -#pragma warning disable EF1001 - var stateManager = changeTracker.Context.GetDependencies().StateManager; + RecursiveDetectChanges(entityEntry); + } + + protected virtual void RecursiveDetectChanges(EntityEntry entityEntry) + { + #pragma warning disable EF1001 + var stateManager = entityEntry.Context.GetDependencies().StateManager; var internalEntityEntityEntry = stateManager.Entries.FirstOrDefault(x => x.Entity == entityEntry.Entity); if (internalEntityEntityEntry == null) { @@ -85,7 +87,12 @@ public class AbpEfCoreNavigationHelper : ITransientDependency continue; } - abpEntityEntry.IsModified = true; + if (!abpEntityEntry.IsModified) + { + abpEntityEntry.IsModified = true; + RecursiveDetectChanges(abpEntityEntry.EntityEntry); + } + var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == foreignKey) ?? abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == foreignKey); if (navigationEntry != null) @@ -119,7 +126,12 @@ public class AbpEfCoreNavigationHelper : ITransientDependency continue; } - abpEntityEntry.IsModified = true; + if (!abpEntityEntry.IsModified) + { + abpEntityEntry.IsModified = true; + RecursiveDetectChanges(abpEntityEntry.EntityEntry); + } + var navigationEntry = abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is INavigation navigationMetadata && navigationMetadata.ForeignKey == inverseForeignKey) ?? abpEntityEntry.NavigationEntries.FirstOrDefault(x => x.NavigationEntry.Metadata is ISkipNavigation skipNavigationMetadata && skipNavigationMetadata.ForeignKey == inverseForeignKey); if (navigationEntry != null) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs index c26ae2bbf1..0aef48e0d1 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Volo.Abp.EntityFrameworkCore.ChangeTrackers; @@ -17,7 +18,7 @@ public class AbpEntityEntry { get { - return _isModified || NavigationEntries.Any(n => n.IsModified); + return _isModified || EntityEntry.State == EntityState.Modified || NavigationEntries.Any(n => n.IsModified); } set { 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 428e0239c3..98364788b3 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 @@ -51,7 +51,7 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule options.Entity(opt => { - opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToOne).Include(p => p.OneToMany).Include(p => p.ManyToMany); + opt.DefaultWithDetailsFunc = q => q.Include(p => p.OneToOne).ThenInclude(x => x.OneToOne).Include(p => p.OneToMany).ThenInclude(x => x.OneToMany).Include(p => p.ManyToMany); }); }); 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 5de4bd6369..225e767d26 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 @@ -70,10 +70,22 @@ public class TestMigrationsDbContext : AbpDbContext modelBuilder.Entity(b => { b.ConfigureByConvention(); - b.OwnsOne(v => v.AppEntityWithValueObjectAddress); + 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(x => x.ManyToMany).UsingEntity(); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); + }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationChildOneToManyId); + }); } } 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 63b57ec147..e668a21e26 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 @@ -103,6 +103,18 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); + }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationChildOneToManyId); + }); + 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 b0c2e4f7bf..e9b9706e0c 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 @@ -36,7 +36,6 @@ public class AppEntityWithValueObjectAddress : ValueObject { public AppEntityWithValueObjectAddress(string country) { - Country = country; } @@ -48,8 +47,14 @@ public class AppEntityWithValueObjectAddress : ValueObject } } - public class AppEntityWithNavigationChildOneToOne : Entity +{ + public string ChildName { get; set; } + + public virtual AppEntityWithNavigationChildOneToOneAndOneToOne OneToOne { get; set; } +} + +public class AppEntityWithNavigationChildOneToOneAndOneToOne : Entity { public string ChildName { get; set; } } @@ -59,6 +64,15 @@ public class AppEntityWithNavigationChildOneToMany : Entity public Guid AppEntityWithNavigationId { get; set; } public string ChildName { get; set; } + + public virtual List OneToMany { get; set; } +} + +public class AppEntityWithNavigationChildOneToManyAndOneToMany : Entity +{ + public Guid AppEntityWithNavigationChildOneToManyId { get; set; } + + public string ChildName { get; set; } } public class AppEntityWithNavigationChildManyToMany : AggregateRoot 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 ddb91e7ba6..c24d4567f4 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 @@ -252,7 +252,11 @@ public abstract class DomainEvents_Tests : TestAppTestBase : TestAppTestBase + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne.OneToOne.ChildName = "OneToOne-ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + LocalEventBus.Subscribe>(data => { throw new Exception("Should not trigger this event"); @@ -294,7 +308,14 @@ public abstract class DomainEvents_Tests : TestAppTestBase() + { + new AppEntityWithNavigationChildOneToManyAndOneToMany() + { + ChildName = "OneToMany-ChildName1" + } + } } }; await AppEntityWithNavigationsRepository.UpdateAsync(entity); @@ -312,6 +333,16 @@ public abstract class DomainEvents_Tests : TestAppTestBase + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany[0].OneToMany[0].ChildName = "OneToMany-ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + LocalEventBus.Subscribe>(data => { throw new Exception("Should not trigger this event"); From 195a9bec2f142eeb2b0ec21fb60581096c7f2454 Mon Sep 17 00:00:00 2001 From: maliming Date: Sat, 20 Apr 2024 13:38:20 +0800 Subject: [PATCH 5/6] Add more unit test cases. --- .../AbpEfCoreNavigationHelper.cs | 21 ++++---- .../Abp/TestApp/Testing/DomainEvents_Tests.cs | 50 ++++++++++++++++--- 2 files changed, 55 insertions(+), 16 deletions(-) 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 4a57f0803c..50a8a121d3 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 @@ -36,7 +36,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return; } - var entryId = GetEntityId(entityEntry); + var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) { return; @@ -81,7 +81,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency continue; } - var entryId = GetEntityId(principal.ToEntityEntry()); + var entryId = GetEntityEntryIdentity(principal.ToEntityEntry()); if (entryId == null || !EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) { continue; @@ -120,7 +120,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency continue; } - var entryId = GetEntityId(principal.ToEntityEntry()); + var entryId = GetEntityEntryIdentity(principal.ToEntityEntry()); if (entryId == null || !EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) { continue; @@ -158,7 +158,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return true; } - var entryId = GetEntityId(entityEntry); + var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) { return false; @@ -169,7 +169,7 @@ public class AbpEfCoreNavigationHelper : ITransientDependency public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, int navigationEntryIndex) { - var entryId = GetEntityId(entityEntry); + var entryId = GetEntityEntryIdentity(entityEntry); if (entryId == null) { return false; @@ -184,11 +184,14 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return navigationEntryProperty != null && navigationEntryProperty.IsModified; } - protected virtual string? GetEntityId(EntityEntry entityEntry) + protected virtual string? GetEntityEntryIdentity(EntityEntry entityEntry) { - return entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1 - ? entryEntity.GetKeys().FirstOrDefault()?.ToString() - : null; + if (entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1) + { + return $"{entityEntry.Metadata.ClrType.FullName}:{entryEntity.GetKeys().FirstOrDefault()}"; + } + + return null; } public void Clear() 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 c24d4567f4..6032d33a2f 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 @@ -187,15 +187,15 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => + LocalEventBus.Subscribe>(_ => { personCreatedEventCount++; return Task.CompletedTask; }); - LocalEventBus.Subscribe>(async data => + LocalEventBus.Subscribe>(async _ => { - entityUpdatedEventTriggered = !entityUpdatedEventTriggered; + entityUpdatedEventTriggered = true; await PersonRepository.InsertAsync(new Person(Guid.NewGuid(), Guid.NewGuid().ToString(), new Random().Next(1, 100))); }); @@ -230,7 +230,7 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => + LocalEventBus.Subscribe>(_ => { throw new Exception("Should not trigger this event"); }); @@ -263,6 +263,18 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(async _ => + { + oneToOneEntityUpdatedEventTriggered = true; + }); + LocalEventBus.Subscribe>(async _ => + { + oneToOneAndOneToOneEntityUpdatedEventTriggered = true; + }); + entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { @@ -271,9 +283,13 @@ public abstract class DomainEvents_Tests : TestAppTestBase { var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); @@ -281,9 +297,11 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => + LocalEventBus.Subscribe>(_ => { throw new Exception("Should not trigger this event"); }); @@ -323,6 +341,18 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(async _ => + { + oneToManyEntityUpdatedEventTriggered = true; + }); + LocalEventBus.Subscribe>(async _ => + { + oneToManyAndOneToManyEntityUpdatedEventTriggered = true; + }); + entityUpdatedEventTriggered = false; await WithUnitOfWorkAsync(async () => { @@ -331,9 +361,13 @@ public abstract class DomainEvents_Tests : TestAppTestBase { var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); @@ -341,9 +375,11 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(data => + LocalEventBus.Subscribe>(_ => { throw new Exception("Should not trigger this event"); }); @@ -439,7 +475,7 @@ public abstract class AbpEntityChangeOptions_DomainEvents_Tests LocalEventBus.Subscribe>(data => { - entityUpdatedEventTriggered = !entityUpdatedEventTriggered; + entityUpdatedEventTriggered = true; return Task.CompletedTask; }); From 030651ad91b71c40e1fa28bdf9400deaaee70f7c Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 21 Apr 2024 10:09:15 +0800 Subject: [PATCH 6/6] Move `Navigation_Changes_Tests` to EF Core test project. --- .../DomainEvents/DomainEvents_Tests.cs | 268 +++++++++++++++++- .../EntityFrameworkCoreTestBase.cs | 4 +- .../Abp/TestApp/Testing/DomainEvents_Tests.cs | 256 ----------------- 3 files changed, 269 insertions(+), 259 deletions(-) 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 978d411634..776e7d4766 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 @@ -26,11 +26,14 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase { protected readonly IRepository AppEntityWithNavigationsRepository; protected readonly ILocalEventBus LocalEventBus; + protected readonly IRepository PersonRepository; + protected bool _loadEntityWithDetails = false; public AbpEfCoreDomainEvents_Tests() { AppEntityWithNavigationsRepository = GetRequiredService>(); LocalEventBus = GetRequiredService(); + PersonRepository = GetRequiredService>(); } protected override void AfterAddApplication(IServiceCollection services) @@ -39,16 +42,279 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase { options.Entity(opt => { - opt.DefaultWithDetailsFunc = q => q; + if (_loadEntityWithDetails) + { + opt.DefaultWithDetailsFunc = q => q; + } }); }); base.AfterAddApplication(services); } + [Fact] + public async Task Should_Trigger_Domain_Events_For_Aggregate_Root_When_Navigation_Changes_Tests() + { + _loadEntityWithDetails = false; + + var entityId = Guid.NewGuid(); + + await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity")); + + var entityUpdatedEventTriggered = false; + var personCreatedEventCount = 0; + var entityUpdatedEventTriggerCount = 0; + + LocalEventBus.Subscribe>(_ => + { + personCreatedEventCount++; + return Task.CompletedTask; + }); + + LocalEventBus.Subscribe>(async _ => + { + entityUpdatedEventTriggered = true; + await PersonRepository.InsertAsync(new Person(Guid.NewGuid(), Guid.NewGuid().ToString(), new Random().Next(1, 100))); + }); + + // Test with simple property + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.Name = Guid.NewGuid().ToString(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // 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(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress.Country = "USA"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + LocalEventBus.Subscribe>(_ => + { + throw new Exception("Should not trigger this event"); + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = null; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // Test with one to one + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName", + OneToOne = new AppEntityWithNavigationChildOneToOneAndOneToOne + { + ChildName = "OneToOne-ChildName" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + var oneToOneEntityUpdatedEventTriggered = false; + var oneToOneAndOneToOneEntityUpdatedEventTriggered = false; + + LocalEventBus.Subscribe>(async _ => + { + oneToOneEntityUpdatedEventTriggered = true; + }); + LocalEventBus.Subscribe>(async _ => + { + oneToOneAndOneToOneEntityUpdatedEventTriggered = true; + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne.ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneAndOneToOneEntityUpdatedEventTriggered.ShouldBeFalse(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + oneToOneEntityUpdatedEventTriggered = false; + oneToOneAndOneToOneEntityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne.OneToOne.ChildName = "OneToOne-ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToOneAndOneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + LocalEventBus.Subscribe>(_ => + { + throw new Exception("Should not trigger this event"); + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = null; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // 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", + OneToMany = new List() + { + new AppEntityWithNavigationChildOneToManyAndOneToMany() + { + ChildName = "OneToMany-ChildName1" + } + } + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + var oneToManyEntityUpdatedEventTriggered = false; + var oneToManyAndOneToManyEntityUpdatedEventTriggered = false; + + LocalEventBus.Subscribe>(async _ => + { + oneToManyEntityUpdatedEventTriggered = true; + }); + LocalEventBus.Subscribe>(async _ => + { + oneToManyAndOneToManyEntityUpdatedEventTriggered = true; + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany[0].ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyAndOneToManyEntityUpdatedEventTriggered.ShouldBeFalse(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + oneToManyEntityUpdatedEventTriggered = false; + oneToManyAndOneToManyEntityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany[0].OneToMany[0].ChildName = "OneToMany-ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); + oneToManyAndOneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + LocalEventBus.Subscribe>(_ => + { + throw new Exception("Should not trigger this event"); + }); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany.Clear(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + // 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(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany[0].ChildName = "ChildName2"; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany.Clear(); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); + } + [Fact] public async Task Should_Trigger_Domain_Events_For_Aggregate_Root_When_EnsureCollectionLoaded_Navigation_Changes_Tests() { + _loadEntityWithDetails = true; + var entityId = Guid.NewGuid(); await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity") diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs index a046c5ade9..119b169638 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/EntityFrameworkCoreTestBase.cs @@ -1,8 +1,8 @@ -using Volo.Abp.Testing; +using Volo.Abp.TestApp.Testing; namespace Volo.Abp.EntityFrameworkCore; -public abstract class EntityFrameworkCoreTestBase : AbpIntegratedTest +public abstract class EntityFrameworkCoreTestBase : TestAppTestBase { protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) { 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 6032d33a2f..7db0ba180d 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 @@ -176,262 +176,6 @@ public abstract class DomainEvents_Tests : TestAppTestBase>(_ => - { - personCreatedEventCount++; - return Task.CompletedTask; - }); - - LocalEventBus.Subscribe>(async _ => - { - entityUpdatedEventTriggered = true; - await PersonRepository.InsertAsync(new Person(Guid.NewGuid(), Guid.NewGuid().ToString(), new Random().Next(1, 100))); - }); - - // Test with simple property - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.Name = Guid.NewGuid().ToString(); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // 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(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.AppEntityWithValueObjectAddress.Country = "USA"; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - LocalEventBus.Subscribe>(_ => - { - throw new Exception("Should not trigger this event"); - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.AppEntityWithValueObjectAddress = null; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // Test with one to one - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToOne = new AppEntityWithNavigationChildOneToOne - { - ChildName = "ChildName", - OneToOne = new AppEntityWithNavigationChildOneToOneAndOneToOne - { - ChildName = "OneToOne-ChildName" - } - }; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - var oneToOneEntityUpdatedEventTriggered = false; - var oneToOneAndOneToOneEntityUpdatedEventTriggered = false; - - LocalEventBus.Subscribe>(async _ => - { - oneToOneEntityUpdatedEventTriggered = true; - }); - LocalEventBus.Subscribe>(async _ => - { - oneToOneAndOneToOneEntityUpdatedEventTriggered = true; - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToOne.ChildName = "ChildName2"; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - oneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); - oneToOneAndOneToOneEntityUpdatedEventTriggered.ShouldBeFalse(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - oneToOneEntityUpdatedEventTriggered = false; - oneToOneAndOneToOneEntityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToOne.OneToOne.ChildName = "OneToOne-ChildName2"; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - oneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); - oneToOneAndOneToOneEntityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - LocalEventBus.Subscribe>(_ => - { - throw new Exception("Should not trigger this event"); - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToOne = null; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // 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", - OneToMany = new List() - { - new AppEntityWithNavigationChildOneToManyAndOneToMany() - { - ChildName = "OneToMany-ChildName1" - } - } - } - }; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - var oneToManyEntityUpdatedEventTriggered = false; - var oneToManyAndOneToManyEntityUpdatedEventTriggered = false; - - LocalEventBus.Subscribe>(async _ => - { - oneToManyEntityUpdatedEventTriggered = true; - }); - LocalEventBus.Subscribe>(async _ => - { - oneToManyAndOneToManyEntityUpdatedEventTriggered = true; - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToMany[0].ChildName = "ChildName2"; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - oneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); - oneToManyAndOneToManyEntityUpdatedEventTriggered.ShouldBeFalse(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - oneToManyEntityUpdatedEventTriggered = false; - oneToManyAndOneToManyEntityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToMany[0].OneToMany[0].ChildName = "OneToMany-ChildName2"; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - oneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); - oneToManyAndOneToManyEntityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - LocalEventBus.Subscribe>(_ => - { - throw new Exception("Should not trigger this event"); - }); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.OneToMany.Clear(); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - // 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(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.ManyToMany[0].ChildName = "ChildName2"; - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - - entityUpdatedEventTriggered = false; - await WithUnitOfWorkAsync(async () => - { - var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); - entity.ManyToMany.Clear(); - await AppEntityWithNavigationsRepository.UpdateAsync(entity); - }); - entityUpdatedEventTriggered.ShouldBeTrue(); - personCreatedEventCount.ShouldBe(++entityUpdatedEventTriggerCount); - } - private class MyCustomEventData { public string Value { get; set; }