From 17c4e6be5364c13fe788216d68dbcab8a5b63fac Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 10 Jan 2025 20:13:10 +0800 Subject: [PATCH] Get the before and after changes of navigation properties. --- .../AbpEfCoreNavigationHelper.cs | 24 +++++++ .../ChangeTrackers/AbpEntityEntry.cs | 64 +++++++++++++++++ .../EntityHistory/EntityHistoryHelper.cs | 54 +++++++++++++- .../Volo/Abp/Auditing/Auditing_Tests.cs | 72 +++++++++++++++++-- 4 files changed, 206 insertions(+), 8 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 d56660be57..63ddc222bd 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 @@ -31,6 +31,14 @@ public class AbpEfCoreNavigationHelper : ITransientDependency protected virtual void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry) { + if (entityEntry.State is EntityState.Unchanged or EntityState.Modified) + { + foreach (var entry in EntityEntries.Values.Where(x => x.NavigationEntries.Any())) + { + entry.UpdateNavigationEntries(); + } + } + if (entityEntry.State != EntityState.Unchanged) { return; @@ -189,6 +197,22 @@ public class AbpEfCoreNavigationHelper : ITransientDependency return navigationEntryProperty != null && navigationEntryProperty.IsModified; } + public virtual AbpNavigationEntry? GetNavigationEntry(EntityEntry entityEntry, int navigationEntryIndex) + { + var entryId = GetEntityEntryIdentity(entityEntry); + if (entryId == null) + { + return null; + } + + if (!EntityEntries.TryGetValue(entryId, out var abpEntityEntry)) + { + return null; + } + + return abpEntityEntry.NavigationEntries.ElementAtOrDefault(navigationEntryIndex); + } + protected virtual string? GetEntityEntryIdentity(EntityEntry entityEntry) { if (entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1) 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 0aef48e0d1..4b8a531db1 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,3 +1,4 @@ +using System.Collections; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -32,6 +33,49 @@ public class AbpEntityEntry EntityEntry = entityEntry; NavigationEntries = EntityEntry.Navigations.Select(x => new AbpNavigationEntry(x, x.Metadata.Name)).ToList(); } + + public void UpdateNavigationEntries() + { + foreach (var navigationEntry in NavigationEntries) + { + if (IsModified || + EntityEntry.State == EntityState.Modified || + navigationEntry.IsModified || + navigationEntry.NavigationEntry.IsModified) + { + continue; + } + + var navigation = EntityEntry.Navigations.FirstOrDefault(n => n.Metadata.Name == navigationEntry.Name); + + var currentValue = AbpNavigationEntry.GetOriginalValue(navigation?.CurrentValue); + if (currentValue == null) + { + continue; + } + + switch (navigationEntry.OriginalValue) + { + case null: + navigationEntry.OriginalValue = currentValue; + break; + case IEnumerable originalValueCollection when currentValue is IEnumerable currentValueCollection: + { + var existingList = originalValueCollection.Cast().ToList(); + var newList = currentValueCollection.Cast().ToList(); + if (newList.Count > existingList.Count) + { + navigationEntry.OriginalValue = currentValue; + } + + break; + } + default: + navigationEntry.OriginalValue = currentValue; + break; + } + } + } } public class AbpNavigationEntry @@ -42,9 +86,29 @@ public class AbpNavigationEntry public bool IsModified { get; set; } + public List? OriginalValue { get; set; } + + public object? CurrentValue => NavigationEntry.CurrentValue; + public AbpNavigationEntry(NavigationEntry navigationEntry, string name) { NavigationEntry = navigationEntry; Name = name; + OriginalValue = GetOriginalValue(navigationEntry.CurrentValue); + } + + public static List? GetOriginalValue(object? currentValue) + { + if (currentValue is null) + { + return null; + } + + if (currentValue is IEnumerable enumerable) + { + return enumerable.Cast().ToList(); + } + + return new List { currentValue }; } } 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 a52ac2f437..e9dd794d73 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 @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -199,10 +200,14 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency { if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { + var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index); + var isCollection = navigationEntry.Metadata.IsCollection; propertyChanges.Add(new EntityPropertyChangeInfo { PropertyName = navigationEntry.Metadata.Name, - PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! + PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, + OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), + NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) }); } } @@ -211,6 +216,53 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + protected virtual string? GetNavigationPropertyValue(object? entity, bool isCollection) + { + switch (entity) + { + case null: + return null; + + case IEntity entryEntity: + var keys = entryEntity.GetKeys(); + if (keys.Length == 0) + { + return null; + } + + var serializedKeys = keys.Length == 1 && !isCollection + ? keys[0]?.ToString() + : JsonSerializer.Serialize(keys); + + return serializedKeys?.TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength); + + case IEnumerable enumerable: + var keysList = new List(); + foreach (var item in enumerable) + { + var id = GetNavigationPropertyValue(item, false); + if (id != null) + { + keysList.Add(id); + } + } + + if (keysList.Count == 0) + { + return null; + } + + var serializedKeysEnumerable = keysList.Count == 1 && !isCollection + ? keysList.First() + : JsonSerializer.Serialize(keysList); + + return serializedKeysEnumerable.TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength); + + default: + return null; + } + } + protected virtual bool IsCreated(EntityEntry entityEntry) { return entityEntry.State == EntityState.Added; 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 954e6222ee..aa11d7ffde 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 @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -512,7 +513,9 @@ public class Auditing_Tests : AbpAuditingTestBase 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)); + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName && + x.EntityChanges[1].PropertyChanges[0].OriginalValue == null && + x.EntityChanges[1].PropertyChanges[0].NewValue == entityId.ToString())); AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 @@ -539,10 +542,13 @@ public class Auditing_Tests : AbpAuditingTestBase 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)); + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName && + x.EntityChanges[1].PropertyChanges[0].OriginalValue == entityId.ToString() && + x.EntityChanges[1].PropertyChanges[0].NewValue == null)); AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + var oneToManyId = ""; using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) @@ -561,6 +567,8 @@ public class Auditing_Tests : AbpAuditingTestBase await repository.UpdateAsync(entity); await uow.CompleteAsync(); await scope.SaveAsync(); + + oneToManyId = entity.OneToMany.First().Id.ToString(); } } @@ -572,36 +580,80 @@ public class Auditing_Tests : AbpAuditingTestBase 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)); + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName && + x.EntityChanges[1].PropertyChanges[0].OriginalValue == null && + x.EntityChanges[1].PropertyChanges[0].NewValue == $"[\"{oneToManyId}\"]")); AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + var newOneToManyId = ""; using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) { var entity = await repository.GetAsync(entityId); - entity.OneToMany = null; + entity.OneToMany.Add(new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName2" + }); await repository.UpdateAsync(entity); await uow.CompleteAsync(); await scope.SaveAsync(); + + newOneToManyId = JsonSerializer.Serialize(entity.OneToMany.Select(x => x.Id).ToList()); } } #pragma warning disable 4014 AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && - x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && 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)); + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName && + x.EntityChanges[1].PropertyChanges[0].OriginalValue == $"[\"{oneToManyId}\"]" && + x.EntityChanges[1].PropertyChanges[0].NewValue == newOneToManyId)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + newOneToManyId = JsonSerializer.Serialize(entity.OneToMany.Select(x => x.Id).ToList()); + + 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 == 3 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName && + x.EntityChanges[2].ChangeType == EntityChangeType.Updated && + x.EntityChanges[2].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[2].PropertyChanges.Count == 1 && + x.EntityChanges[2].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) && + x.EntityChanges[2].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName && + x.EntityChanges[2].PropertyChanges[0].OriginalValue == newOneToManyId && + x.EntityChanges[2].PropertyChanges[0].NewValue == null)); AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 + var manyToManyId = ""; using (var scope = _auditingManager.BeginScope()) { using (var uow = _unitOfWorkManager.Begin()) @@ -619,6 +671,8 @@ public class Auditing_Tests : AbpAuditingTestBase await repository.UpdateAsync(entity); await uow.CompleteAsync(); await scope.SaveAsync(); + + manyToManyId = entity.ManyToMany.First().Id.ToString(); } } @@ -630,7 +684,9 @@ public class Auditing_Tests : AbpAuditingTestBase x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && x.EntityChanges[1].PropertyChanges.Count == 1 && x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) && - x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName && + x.EntityChanges[1].PropertyChanges[0].OriginalValue == null && + x.EntityChanges[1].PropertyChanges[0].NewValue == $"[\"{manyToManyId}\"]")); #pragma warning restore 4014 @@ -655,6 +711,8 @@ public class Auditing_Tests : AbpAuditingTestBase 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[0].PropertyChanges[0].OriginalValue == $"[\"{manyToManyId}\"]" && + x.EntityChanges[0].PropertyChanges[0].NewValue == null && x.EntityChanges[1].ChangeType == EntityChangeType.Updated && x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigationChildManyToMany).FullName &&