diff --git a/docs/en/Audit-Logging.md b/docs/en/Audit-Logging.md
index e2d577c756..ffacc1b10f 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 30b32c3587..f9f5284b29 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 dc3754be3b..a664971057 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 class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
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 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
}
}
- if (entityEntry.State == EntityState.Unchanged)
+ if (Options.SaveEntityHistoryWhenNavigationChanges && entityEntry.State == EntityState.Unchanged)
{
foreach (var navigation in entityEntry.Navigations)
{
@@ -227,7 +227,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency
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 7154fd805c..a5060dc595 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 class Auditing_Tests : AbpAuditingTestBase
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 class Auditing_Tests : AbpAuditingTestBase
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 class Auditing_Tests : AbpAuditingTestBase
}
#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 class Auditing_Tests : AbpAuditingTestBase
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 class Auditing_Tests : AbpAuditingTestBase
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 class Auditing_Tests : AbpAuditingTestBase
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 class Auditing_DisableLogActionInfo_Tests : Auditing_Tests
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
+ }
+}