From 155b7449b7e7844f640429d0b80f8fd52ec53cd0 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 8 Jan 2024 16:49:44 +0800 Subject: [PATCH] Add `SaveEntityHistoryWhenNavigationChanges` to `AbpAuditingOptions `. Resolve #18701 --- docs/en/Audit-Logging.md | 1 + .../Volo/Abp/Auditing/AbpAuditingOptions.cs | 6 + .../EntityHistory/EntityHistoryHelper.cs | 6 +- .../Volo/Abp/Auditing/Auditing_Tests.cs | 255 +++++++++++++++++- 4 files changed, 251 insertions(+), 17 deletions(-) diff --git a/docs/en/Audit-Logging.md b/docs/en/Audit-Logging.md index e2d577c7561..ffacc1b10f3 100644 --- a/docs/en/Audit-Logging.md +++ b/docs/en/Audit-Logging.md @@ -46,6 +46,7 @@ Here, a list of the options you can configure: * `ApplicationName`: If multiple applications are saving audit logs into a single database, set this property to your application name, so you can distinguish the logs of different applications. If you don't set, it will set from the `IApplicationInfoAccessor.ApplicationName` value, which is the entry assembly name by default. * `IgnoredTypes`: A list of `Type`s to be ignored for audit logging. If this is an entity type, changes for this type of entities will not be saved. This list is also used while serializing the action parameters. * `EntityHistorySelectors`: A list of selectors those are used to determine if an entity type is selected for saving the entity change. See the section below for details. +* `SaveEntityHistoryWhenNavigationChanges` (default: `true`): If you set to true, it will save entity changes to audit log when any navigation property changes. * `Contributors`: A list of `AuditLogContributor` implementations. A contributor is a way of extending the audit log system. See the "Audit Log Contributors" section below. * `AlwaysLogSelectors`: A list of selectors to save the audit logs for the matched criteria. diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs index 30b32c3587b..f9f5284b29c 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AbpAuditingOptions.cs @@ -53,6 +53,12 @@ public class AbpAuditingOptions public IEntityHistorySelectorList EntityHistorySelectors { get; } + /// + /// Default: true. + /// Save entity changes to audit log when any navigation property changes. + /// + public bool SaveEntityHistoryWhenNavigationChanges { get; set; } = true; + //TODO: Move this to asp.net core layer or convert it to a more dynamic strategy? /// /// Default: false. 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 dc3754be3bc..a6649710576 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 @@ -85,7 +85,7 @@ public virtual List CreateChangeList(ICollection case EntityState.Modified: changeType = IsDeleted(entityEntry) ? EntityChangeType.Deleted : EntityChangeType.Updated; break; - case EntityState.Unchanged: + case EntityState.Unchanged when Options.SaveEntityHistoryWhenNavigationChanges: changeType = EntityChangeType.Updated; // Navigation property changes. break; case EntityState.Detached: @@ -186,7 +186,7 @@ protected virtual List GetPropertyChanges(EntityEntry } } - if (entityEntry.State == EntityState.Unchanged) + if (Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged) { foreach (var navigation in entityEntry.Navigations) { @@ -227,7 +227,7 @@ protected virtual bool ShouldSaveEntityHistory(EntityEntry entityEntry, bool def return false; } - if (entityEntry.State == EntityState.Unchanged) + if (Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged) { if (entityEntry.Navigations.Any(navigationEntry => navigationEntry.IsModified)) { 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 7154fd805c1..a5060dc5959 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 @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; @@ -392,6 +391,7 @@ public virtual async Task Should_Write_AuditLog_For_ValueObject_Entity() x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && x.EntityChanges[2].ChangeType == EntityChangeType.Deleted && x.EntityChanges[2].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName)); + AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 using (var scope = _auditingManager.BeginScope()) @@ -421,7 +421,7 @@ public virtual async Task Should_Write_AuditLog_For_ValueObject_Entity() x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && x.EntityChanges[1].PropertyChanges.Count == 1 && x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObject.AppEntityWithValueObjectAddress))); - + AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 using (var scope = _auditingManager.BeginScope()) @@ -439,22 +439,16 @@ public virtual async Task Should_Write_AuditLog_For_ValueObject_Entity() } #pragma warning disable 4014 - AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && - x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && - x.EntityChanges[0].PropertyChanges.Count == 1 && - x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && - x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" && - x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"" && - - x.EntityChanges[1].ChangeType == EntityChangeType.Updated && - x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName)); - + x.EntityChanges[0].PropertyChanges.Count == 2 && + x.EntityChanges[0].PropertyChanges.All(p => p.NewValue == null))); #pragma warning restore 4014 } [Fact] - public virtual async Task Should_Write_AuditLog_For_Navigations_Changes() + public virtual async Task Should_Write_AuditLog_For_Navigation_Changes() { var entityId = Guid.NewGuid(); var repository = ServiceProvider.GetRequiredService>(); @@ -484,6 +478,7 @@ public virtual async Task Should_Write_AuditLog_For_Navigations_Changes() x.EntityChanges[0].PropertyChanges[0].NewValue == "\"test full name\"" && x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.FullName) && x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 using (var scope = _auditingManager.BeginScope()) @@ -513,6 +508,7 @@ public virtual async Task Should_Write_AuditLog_For_Navigations_Changes() x.EntityChanges[1].PropertyChanges.Count == 1 && x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToOne) && x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName)); + AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 using (var scope = _auditingManager.BeginScope()) @@ -545,7 +541,7 @@ public virtual async Task Should_Write_AuditLog_For_Navigations_Changes() x.EntityChanges[1].PropertyChanges.Count == 1 && x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) && x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); - + AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 using (var scope = _auditingManager.BeginScope()) @@ -604,3 +600,234 @@ public async Task Should_DisableLogActionInfo() await AuditingStore.Received().SaveAsync(Arg.Is(x => x.Actions.IsNullOrEmpty())); } } + +public class Auditing_SaveEntityHistoryWhenNavigationChanges_Tests : AbpAuditingTestBase +{ + protected IAuditingStore AuditingStore; + private IAuditingManager _auditingManager; + private IUnitOfWorkManager _unitOfWorkManager; + + public Auditing_SaveEntityHistoryWhenNavigationChanges_Tests() + { + _auditingManager = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + AuditingStore = Substitute.For(); + services.Replace(ServiceDescriptor.Singleton(AuditingStore)); + + services.Configure(options => + { + options.SaveEntityHistoryWhenNavigationChanges = false; + }); + } + + [Fact] + public virtual async Task Should_Write_AuditLog_For_ValueObject_Entity() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + await repository.InsertAsync(new AppEntityWithValueObject(entityId, "test name", new AppEntityWithValueObjectAddress("USA"))); + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.Name = "test name 2"; + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("England"); + + 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.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[2].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[2].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.AppEntityWithValueObjectAddress.Country = "Germany"; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"")); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.AppEntityWithValueObjectAddress = null; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Deleted && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[0].PropertyChanges.Count == 2 && + x.EntityChanges[0].PropertyChanges.All(p => p.NewValue == null))); +#pragma warning restore 4014 + } + + [Fact] + public virtual async Task Should_Not_Write_AuditLog_For_Navigation_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + await repository.InsertAsync(new AppEntityWithNavigations(entityId, "test name")); + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.FullName = "test full name"; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"test name\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"test full name\"" && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.FullName) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName" + }; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildOneToOne.ChildName) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1" + } + }; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName && + x.EntityChanges[0].PropertyChanges.Count == 2 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildOneToMany.AppEntityWithNavigationId) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(Guid).FullName && + x.EntityChanges[0].PropertyChanges[1].PropertyName == nameof(AppEntityWithNavigationChildOneToMany.ChildName) && + x.EntityChanges[0].PropertyChanges[1].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildManyToMany).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildManyToMany.ChildName) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + +#pragma warning restore 4014 + } +}