Skip to content

Commit 02bb304

Browse files
authored
Merge pull request #21873 from abpframework/21866
Get the before and after changes of navigation properties.
2 parents f7d960a + 246a6f5 commit 02bb304

File tree

10 files changed

+316
-11
lines changed

10 files changed

+316
-11
lines changed

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEfCoreNavigationHelper.cs

+24
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ public virtual void ChangeTracker_StateChanged(object? sender, EntityStateChange
3131

3232
protected virtual void EntityEntryTrackedOrStateChanged(EntityEntry entityEntry)
3333
{
34+
if (entityEntry.State is EntityState.Unchanged or EntityState.Modified)
35+
{
36+
foreach (var entry in EntityEntries.Values.Where(x => x.NavigationEntries.Any()))
37+
{
38+
entry.UpdateNavigationEntries();
39+
}
40+
}
41+
3442
if (entityEntry.State != EntityState.Unchanged)
3543
{
3644
return;
@@ -189,6 +197,22 @@ public virtual bool IsNavigationEntryModified(EntityEntry entityEntry, int? navi
189197
return navigationEntryProperty != null && navigationEntryProperty.IsModified;
190198
}
191199

200+
public virtual AbpNavigationEntry? GetNavigationEntry(EntityEntry entityEntry, int navigationEntryIndex)
201+
{
202+
var entryId = GetEntityEntryIdentity(entityEntry);
203+
if (entryId == null)
204+
{
205+
return null;
206+
}
207+
208+
if (!EntityEntries.TryGetValue(entryId, out var abpEntityEntry))
209+
{
210+
return null;
211+
}
212+
213+
return abpEntityEntry.NavigationEntries.ElementAtOrDefault(navigationEntryIndex);
214+
}
215+
192216
protected virtual string? GetEntityEntryIdentity(EntityEntry entityEntry)
193217
{
194218
if (entityEntry.Entity is IEntity entryEntity && entryEntity.GetKeys().Length == 1)

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ChangeTrackers/AbpEntityEntry.cs

+64
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections;
12
using System.Collections.Generic;
23
using System.Linq;
34
using Microsoft.EntityFrameworkCore;
@@ -32,6 +33,49 @@ public AbpEntityEntry(string id, EntityEntry entityEntry)
3233
EntityEntry = entityEntry;
3334
NavigationEntries = EntityEntry.Navigations.Select(x => new AbpNavigationEntry(x, x.Metadata.Name)).ToList();
3435
}
36+
37+
public void UpdateNavigationEntries()
38+
{
39+
foreach (var navigationEntry in NavigationEntries)
40+
{
41+
if (IsModified ||
42+
EntityEntry.State == EntityState.Modified ||
43+
navigationEntry.IsModified ||
44+
navigationEntry.NavigationEntry.IsModified)
45+
{
46+
continue;
47+
}
48+
49+
var navigation = EntityEntry.Navigations.FirstOrDefault(n => n.Metadata.Name == navigationEntry.Name);
50+
51+
var currentValue = AbpNavigationEntry.GetOriginalValue(navigation?.CurrentValue);
52+
if (currentValue == null)
53+
{
54+
continue;
55+
}
56+
57+
switch (navigationEntry.OriginalValue)
58+
{
59+
case null:
60+
navigationEntry.OriginalValue = currentValue;
61+
break;
62+
case IEnumerable originalValueCollection when currentValue is IEnumerable currentValueCollection:
63+
{
64+
var existingList = originalValueCollection.Cast<object?>().ToList();
65+
var newList = currentValueCollection.Cast<object?>().ToList();
66+
if (newList.Count > existingList.Count)
67+
{
68+
navigationEntry.OriginalValue = currentValue;
69+
}
70+
71+
break;
72+
}
73+
default:
74+
navigationEntry.OriginalValue = currentValue;
75+
break;
76+
}
77+
}
78+
}
3579
}
3680

3781
public class AbpNavigationEntry
@@ -42,9 +86,29 @@ public class AbpNavigationEntry
4286

4387
public bool IsModified { get; set; }
4488

89+
public List<object>? OriginalValue { get; set; }
90+
91+
public object? CurrentValue => NavigationEntry.CurrentValue;
92+
4593
public AbpNavigationEntry(NavigationEntry navigationEntry, string name)
4694
{
4795
NavigationEntry = navigationEntry;
4896
Name = name;
97+
OriginalValue = GetOriginalValue(navigationEntry.CurrentValue);
98+
}
99+
100+
public static List<object>? GetOriginalValue(object? currentValue)
101+
{
102+
if (currentValue is null)
103+
{
104+
return null;
105+
}
106+
107+
if (currentValue is IEnumerable enumerable)
108+
{
109+
return enumerable.Cast<object>().ToList();
110+
}
111+
112+
return new List<object> { currentValue };
49113
}
50114
}

framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs

+45-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Reflection;
@@ -193,16 +194,20 @@ protected virtual List<EntityPropertyChangeInfo> GetPropertyChanges(EntityEntry
193194
}
194195
}
195196

196-
if (Options.SaveEntityHistoryWhenNavigationChanges && AbpEfCoreNavigationHelper != null)
197+
if (AbpEfCoreNavigationHelper != null)
197198
{
198199
foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i )))
199200
{
200201
if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index))
201202
{
203+
var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index);
204+
var isCollection = navigationEntry.Metadata.IsCollection;
202205
propertyChanges.Add(new EntityPropertyChangeInfo
203206
{
204207
PropertyName = navigationEntry.Metadata.Name,
205-
PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!
208+
PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!,
209+
OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection),
210+
NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection)
206211
});
207212
}
208213
}
@@ -211,6 +216,44 @@ protected virtual List<EntityPropertyChangeInfo> GetPropertyChanges(EntityEntry
211216
return propertyChanges;
212217
}
213218

219+
protected virtual string? GetNavigationPropertyValue(object? entity, bool isCollection)
220+
{
221+
switch (entity)
222+
{
223+
case null:
224+
return null;
225+
226+
case IEntity entryEntity:
227+
var keys = entryEntity.GetKeys();
228+
return keys.Length == 0 ? null : string.Join(", ",keys).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength);
229+
230+
case IEnumerable enumerable:
231+
var keysList = new List<string>();
232+
foreach (var item in enumerable)
233+
{
234+
var id = GetNavigationPropertyValue(item, false);
235+
if (id != null)
236+
{
237+
keysList.Add(id);
238+
}
239+
}
240+
241+
if (keysList.Count == 0)
242+
{
243+
return null;
244+
}
245+
246+
var serializedKeysEnumerable = keysList.Count == 1 && !isCollection
247+
? keysList.First()
248+
: JsonSerializer.Serialize(keysList);
249+
250+
return serializedKeysEnumerable.TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength);
251+
252+
default:
253+
return null;
254+
}
255+
}
256+
214257
protected virtual bool IsCreated(EntityEntry entityEntry)
215258
{
216259
return entityEntry.State == EntityState.Added;

framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs

+65-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Text.Json;
45
using System.Threading.Tasks;
56
using Microsoft.Extensions.DependencyInjection;
67
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -512,7 +513,9 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
512513
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
513514
x.EntityChanges[1].PropertyChanges.Count == 1 &&
514515
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToOne) &&
515-
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName));
516+
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName &&
517+
x.EntityChanges[1].PropertyChanges[0].OriginalValue == null &&
518+
x.EntityChanges[1].PropertyChanges[0].NewValue == entityId.ToString()));
516519
AuditingStore.ClearReceivedCalls();
517520
#pragma warning restore 4014
518521

@@ -539,10 +542,13 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
539542
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
540543
x.EntityChanges[1].PropertyChanges.Count == 1 &&
541544
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToOne) &&
542-
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName));
545+
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName &&
546+
x.EntityChanges[1].PropertyChanges[0].OriginalValue == entityId.ToString() &&
547+
x.EntityChanges[1].PropertyChanges[0].NewValue == null));
543548
AuditingStore.ClearReceivedCalls();
544549
#pragma warning restore 4014
545550

551+
var oneToManyId = "";
546552
using (var scope = _auditingManager.BeginScope())
547553
{
548554
using (var uow = _unitOfWorkManager.Begin())
@@ -561,6 +567,8 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
561567
await repository.UpdateAsync(entity);
562568
await uow.CompleteAsync();
563569
await scope.SaveAsync();
570+
571+
oneToManyId = entity.OneToMany.First().Id.ToString();
564572
}
565573
}
566574

@@ -572,36 +580,80 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
572580
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
573581
x.EntityChanges[1].PropertyChanges.Count == 1 &&
574582
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) &&
575-
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildOneToMany>).FullName));
583+
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildOneToMany>).FullName &&
584+
x.EntityChanges[1].PropertyChanges[0].OriginalValue == null &&
585+
x.EntityChanges[1].PropertyChanges[0].NewValue == $"[\"{oneToManyId}\"]"));
576586
AuditingStore.ClearReceivedCalls();
577587
#pragma warning restore 4014
578588

589+
var newOneToManyId = "";
579590
using (var scope = _auditingManager.BeginScope())
580591
{
581592
using (var uow = _unitOfWorkManager.Begin())
582593
{
583594
var entity = await repository.GetAsync(entityId);
584595

585-
entity.OneToMany = null;
596+
entity.OneToMany.Add(new AppEntityWithNavigationChildOneToMany
597+
{
598+
AppEntityWithNavigationId = entity.Id,
599+
ChildName = "ChildName2"
600+
});
586601

587602
await repository.UpdateAsync(entity);
588603
await uow.CompleteAsync();
589604
await scope.SaveAsync();
605+
606+
newOneToManyId = JsonSerializer.Serialize(entity.OneToMany.Select(x => x.Id).ToList());
590607
}
591608
}
592609

593610
#pragma warning disable 4014
594611
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 2 &&
595-
x.EntityChanges[0].ChangeType == EntityChangeType.Deleted &&
612+
x.EntityChanges[0].ChangeType == EntityChangeType.Created &&
596613
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName &&
597614
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
598615
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
599616
x.EntityChanges[1].PropertyChanges.Count == 1 &&
600617
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) &&
601-
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildOneToMany>).FullName));
618+
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildOneToMany>).FullName &&
619+
x.EntityChanges[1].PropertyChanges[0].OriginalValue == $"[\"{oneToManyId}\"]" &&
620+
x.EntityChanges[1].PropertyChanges[0].NewValue == newOneToManyId));
621+
AuditingStore.ClearReceivedCalls();
622+
#pragma warning restore 4014
623+
624+
using (var scope = _auditingManager.BeginScope())
625+
{
626+
using (var uow = _unitOfWorkManager.Begin())
627+
{
628+
var entity = await repository.GetAsync(entityId);
629+
630+
newOneToManyId = JsonSerializer.Serialize(entity.OneToMany.Select(x => x.Id).ToList());
631+
632+
entity.OneToMany = null;
633+
634+
await repository.UpdateAsync(entity);
635+
await uow.CompleteAsync();
636+
await scope.SaveAsync();
637+
}
638+
}
639+
640+
#pragma warning disable 4014
641+
AuditingStore.Received().SaveAsync(Arg.Is<AuditLogInfo>(x => x.EntityChanges.Count == 3 &&
642+
x.EntityChanges[0].ChangeType == EntityChangeType.Deleted &&
643+
x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName &&
644+
x.EntityChanges[1].ChangeType == EntityChangeType.Deleted &&
645+
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName &&
646+
x.EntityChanges[2].ChangeType == EntityChangeType.Updated &&
647+
x.EntityChanges[2].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
648+
x.EntityChanges[2].PropertyChanges.Count == 1 &&
649+
x.EntityChanges[2].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) &&
650+
x.EntityChanges[2].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildOneToMany>).FullName &&
651+
x.EntityChanges[2].PropertyChanges[0].OriginalValue == newOneToManyId &&
652+
x.EntityChanges[2].PropertyChanges[0].NewValue == null));
602653
AuditingStore.ClearReceivedCalls();
603654
#pragma warning restore 4014
604655

656+
var manyToManyId = "";
605657
using (var scope = _auditingManager.BeginScope())
606658
{
607659
using (var uow = _unitOfWorkManager.Begin())
@@ -619,6 +671,8 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
619671
await repository.UpdateAsync(entity);
620672
await uow.CompleteAsync();
621673
await scope.SaveAsync();
674+
675+
manyToManyId = entity.ManyToMany.First().Id.ToString();
622676
}
623677
}
624678

@@ -630,7 +684,9 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
630684
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName &&
631685
x.EntityChanges[1].PropertyChanges.Count == 1 &&
632686
x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) &&
633-
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildManyToMany>).FullName));
687+
x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildManyToMany>).FullName &&
688+
x.EntityChanges[1].PropertyChanges[0].OriginalValue == null &&
689+
x.EntityChanges[1].PropertyChanges[0].NewValue == $"[\"{manyToManyId}\"]"));
634690

635691
#pragma warning restore 4014
636692

@@ -655,6 +711,8 @@ public virtual async Task Should_Write_AuditLog_For_Navigation_Changes()
655711
x.EntityChanges[0].PropertyChanges.Count == 1 &&
656712
x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) &&
657713
x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(List<AppEntityWithNavigationChildManyToMany>).FullName &&
714+
x.EntityChanges[0].PropertyChanges[0].OriginalValue == $"[\"{manyToManyId}\"]" &&
715+
x.EntityChanges[0].PropertyChanges[0].NewValue == null &&
658716

659717
x.EntityChanges[1].ChangeType == EntityChangeType.Updated &&
660718
x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigationChildManyToMany).FullName &&

modules/audit-logging/src/Volo.Abp.AuditLogging.Domain.Shared/Volo/Abp/AuditLogging/Localization/en.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"ChangeType": "Change type",
4949
"ChangeTime": "Time",
5050
"NewValue": "New value",
51-
"OriginalValue": "Original value",
51+
"OriginalValue": "Old value",
5252
"PropertyName": "Property name",
5353
"PropertyTypeFullName": "Property Type Full Name",
5454
"Yes": "Yes",

0 commit comments

Comments
 (0)