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 6a5b608c76..07ea443fdf 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -310,7 +310,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeEventHelper.PublishEntityUpdatedEvent(entityEntry.Entity); } } - else if (entityEntry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + else if (GetAllPropertyEntries(entityEntry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { if (IsOnlyForeignKeysModified(entityEntry)) { @@ -446,7 +446,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; case EntityState.Modified: - if (entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + if (GetAllPropertyEntries(entry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { if (IsOnlyForeignKeysModified(entry)) { @@ -454,7 +454,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, break; } - var modifiedProperties = entry.Properties.Where(x => x.IsModified).ToList(); + var modifiedProperties = GetAllPropertyEntries(entry).Where(x => x.IsModified).ToList(); var disableAuditingAttributes = modifiedProperties.Select(x => x.Metadata.PropertyInfo?.GetCustomAttribute()).ToList(); if (disableAuditingAttributes.Any(x => x == null || x.UpdateModificationProps)) { @@ -501,9 +501,36 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } + protected virtual IEnumerable GetAllPropertyEntries(EntityEntry entry) + { + return entry.Properties.Concat(GetAllComplexPropertyEntries(entry.ComplexProperties)); + } + + protected virtual IEnumerable GetAllComplexPropertyEntries(IEnumerable complexPropertyEntries) + { + foreach (var complexPropertyEntry in complexPropertyEntries) + { + var complexPropertyInfo = complexPropertyEntry.Metadata.PropertyInfo; + if (complexPropertyInfo != null && complexPropertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + continue; + } + + foreach (var propertyEntry in complexPropertyEntry.Properties) + { + yield return propertyEntry; + } + + foreach (var nestedPropertyEntry in GetAllComplexPropertyEntries(complexPropertyEntry.ComplexProperties)) + { + yield return nestedPropertyEntry; + } + } + } + protected virtual bool IsOnlyForeignKeysModified(EntityEntry entry) { - return entry.Properties.Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && + return GetAllPropertyEntries(entry).Where(x => x.IsModified).All(x => x.Metadata.IsForeignKey() && (x.CurrentValue == null || x.OriginalValue?.ToString() == x.CurrentValue?.ToString())); } @@ -662,7 +689,7 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, bool forceApply = false) { if (forceApply || - entry.Properties.Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) + GetAllPropertyEntries(entry).Any(x => x.IsModified && (x.Metadata.ValueGenerated == ValueGenerated.Never || x.Metadata.ValueGenerated == ValueGenerated.OnAdd))) { IncrementEntityVersionProperty(entry); SetModificationAuditProperties(entry); 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 75e82976b4..9a1e8e3cdf 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 @@ -184,6 +184,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency var properties = entityEntry.Metadata.GetProperties(); var isCreated = IsCreated(entityEntry); var isDeleted = IsDeleted(entityEntry); + var isSoftDeleted = IsSoftDeleted(entityEntry); foreach (var property in properties) { @@ -193,7 +194,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } var propertyEntry = entityEntry.Property(property.Name); - if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry)) + if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !isSoftDeleted) { var propertyType = DeterminePropertyTypeFromEntry(property, propertyEntry); @@ -207,6 +208,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } + foreach (var complexPropertyEntry in entityEntry.ComplexProperties) + { + AddComplexPropertyChanges( + complexPropertyEntry, + propertyChanges, + isCreated, + isDeleted, + isSoftDeleted, + parentPath: null); + } + if (AbpEfCoreNavigationHelper == null) { return propertyChanges; @@ -250,6 +262,52 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + protected virtual void AddComplexPropertyChanges( + ComplexPropertyEntry complexPropertyEntry, + List propertyChanges, + bool isCreated, + bool isDeleted, + bool isSoftDeleted, + string? parentPath) + { + var complexPropertyInfo = complexPropertyEntry.Metadata.PropertyInfo; + if (complexPropertyInfo != null && complexPropertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + return; + } + + var complexPropertyPath = parentPath == null + ? complexPropertyEntry.Metadata.Name + : $"{parentPath}.{complexPropertyEntry.Metadata.Name}"; + + foreach (var propertyEntry in complexPropertyEntry.Properties) + { + if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !isSoftDeleted) + { + var propertyType = DeterminePropertyTypeFromEntry(propertyEntry.Metadata, propertyEntry); + + propertyChanges.Add(new EntityPropertyChangeInfo + { + NewValue = isDeleted ? null : JsonSerializer.Serialize(propertyEntry.CurrentValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), + OriginalValue = isCreated ? null : JsonSerializer.Serialize(propertyEntry.OriginalValue!).TruncateWithPostfix(EntityPropertyChangeInfo.MaxValueLength), + PropertyName = $"{complexPropertyPath}.{propertyEntry.Metadata.Name}", + PropertyTypeFullName = propertyType.FullName! + }); + } + } + + foreach (var nestedComplexPropertyEntry in complexPropertyEntry.ComplexProperties) + { + AddComplexPropertyChanges( + nestedComplexPropertyEntry, + propertyChanges, + isCreated, + isDeleted, + isSoftDeleted, + complexPropertyPath); + } + } + /// /// Determines the CLR type of a property based on its EF Core metadata and the values in the given . /// @@ -262,7 +320,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency /// . If both values are null, the declared CLR type /// (which may remain ) is returned. /// - protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) + protected virtual Type DeterminePropertyTypeFromEntry(IReadOnlyPropertyBase property, PropertyEntry propertyEntry) { var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs index 800b73544d..1ab79125e6 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/AbpAuditingTestModule.cs @@ -61,6 +61,7 @@ public class AbpAuditingTestModule : AbpModule ); options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty))); + options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithComplexProperty), type => type == typeof(AppEntityWithComplexProperty))); }); context.Services.AddType(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs new file mode 100644 index 0000000000..be399318cd --- /dev/null +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithComplexProperty.cs @@ -0,0 +1,37 @@ +using System; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Volo.Abp.Auditing.App.Entities; + +public class AppEntityWithComplexProperty : FullAuditedAggregateRoot +{ + public string Name { get; set; } + + public AppEntityContactInformation ContactInformation { get; set; } + + [DisableAuditing] + public AppEntityContactInformation DisabledContactInformation { get; set; } + + public AppEntityWithComplexProperty() + { + } + + public AppEntityWithComplexProperty(Guid id, string name) + : base(id) + { + Name = name; + } +} + +public class AppEntityContactInformation +{ + public string Street { get; set; } = string.Empty; + + public AppEntityContactLocation Location { get; set; } = new(); +} + +public class AppEntityContactLocation +{ + public string City { get; set; } = string.Empty; +} diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs index e8950880d7..a4d0564e90 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs @@ -31,6 +31,7 @@ public class AbpAuditingTestDbContext : AbpDbContext public DbSet AppEntityWithNavigationChildOneToMany { get; set; } public DbSet AppEntityWithNavigationsAndDisableAuditing { get; set; } public DbSet EntitiesWithObjectProperty { get; set; } + public DbSet AppEntitiesWithComplexProperty { get; set; } public AbpAuditingTestDbContext(DbContextOptions options) : base(options) @@ -77,5 +78,27 @@ public class AbpAuditingTestDbContext : AbpDbContext ); }); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.ComplexProperty(x => x.ContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, lb => + { + lb.Property(x => x.City).IsRequired(); + }); + }); + + b.ComplexProperty(x => x.DisabledContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, lb => + { + lb.Property(x => x.City).IsRequired(); + }); + }); + }); } } 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 637b9b4d97..d9667f23e2 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 @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; +using Shouldly; using Volo.Abp.Auditing.App.Entities; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; @@ -820,6 +821,167 @@ public class Auditing_Tests : AbpAuditingTestBase AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 } + + [Fact] + public async Task Should_Write_AuditLog_For_Complex_Property_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = new AppEntityWithComplexProperty(entityId, "Test Entity") + { + ContactInformation = new AppEntityContactInformation + { + Street = "First Street", + Location = new AppEntityContactLocation + { + City = "First City" + } + }, + DisabledContactInformation = new AppEntityContactInformation + { + Street = "Disabled Street", + Location = new AppEntityContactLocation + { + City = "Disabled City" + } + } + }; + + await repository.InsertAsync(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(AppEntityWithComplexProperty).FullName && + x.EntityChanges[0].PropertyChanges.Count == 3 && + x.EntityChanges[0].PropertyChanges.Any(pc => + pc.PropertyName == nameof(AppEntityWithComplexProperty.Name) && + pc.OriginalValue == null && + pc.NewValue == "\"Test Entity\"" && + pc.PropertyTypeFullName == typeof(string).FullName) && + x.EntityChanges[0].PropertyChanges.Any(pc => + pc.PropertyName == "ContactInformation.Street" && + pc.OriginalValue == null && + pc.NewValue == "\"First Street\"" && + pc.PropertyTypeFullName == typeof(string).FullName) && + x.EntityChanges[0].PropertyChanges.Any(pc => + pc.PropertyName == "ContactInformation.Location.City" && + pc.OriginalValue == null && + pc.NewValue == "\"First City\"" && + pc.PropertyTypeFullName == typeof(string).FullName) && + x.EntityChanges[0].PropertyChanges.All(pc => + !pc.PropertyName.StartsWith(nameof(AppEntityWithComplexProperty.DisabledContactInformation))))); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.ContactInformation.Location.City = "Updated City"; + entity.DisabledContactInformation.Street = "Updated Disabled Street"; + + 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(AppEntityWithComplexProperty).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == "ContactInformation.Location.City" && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"First City\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Updated City\"" && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); +#pragma warning restore 4014 + } + + [Fact] + public async Task Should_Not_Update_Modification_Audit_Properties_When_Only_Disabled_Complex_Property_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = new AppEntityWithComplexProperty(entityId, "Test Entity") + { + ContactInformation = new AppEntityContactInformation + { + Street = "First Street", + Location = new AppEntityContactLocation + { + City = "First City" + } + }, + DisabledContactInformation = new AppEntityContactInformation + { + Street = "Disabled Street", + Location = new AppEntityContactLocation + { + City = "Disabled City" + } + } + }; + + await repository.InsertAsync(entity); + + await uow.CompleteAsync(); + } + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.Name = "Updated Test Entity"; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + } + + DateTime? lastModificationTime; + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + lastModificationTime = entity.LastModificationTime; + lastModificationTime.ShouldNotBeNull(); + await uow.CompleteAsync(); + } + + await Task.Delay(10); + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.DisabledContactInformation.Street = "Updated Disabled Street"; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + } + + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + entity.LastModificationTime.ShouldBe(lastModificationTime); + await uow.CompleteAsync(); + } + } } public class Auditing_DisableLogActionInfo_Tests : Auditing_Tests diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs index a54b1656bf..3fe863c046 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/Auditing_Tests.cs @@ -6,6 +6,7 @@ using NSubstitute; using Shouldly; using Volo.Abp.Domain.Entities.Events; using Volo.Abp.TestApp; +using Volo.Abp.TestApp.Domain; using Volo.Abp.TestApp.Testing; using Xunit; @@ -85,6 +86,33 @@ public class Auditing_Tests : Auditing_Tests })); } + [Fact] + public async Task Should_Set_Modification_If_Complex_Properties_Changed() + { + var city = Guid.NewGuid().ToString(); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.City = city; + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.ContactInformation.ShouldNotBeNull(); + douglas.ContactInformation!.Location.City.ShouldBe(city); + douglas.LastModificationTime.ShouldNotBeNull(); + douglas.LastModificationTime.Value.ShouldBeLessThanOrEqualTo(Clock.Now); + douglas.LastModifierId.ShouldBe(CurrentUserId); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + [Fact] public async Task Should_Not_Set_Modification_If_Properties_HasDisableAuditing_UpdateModificationProps() { @@ -106,6 +134,50 @@ public class Auditing_Tests : Auditing_Tests EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); } + [Fact] + public async Task Should_Not_Set_Modification_If_ComplexProperties_HasDisableAuditing_UpdateModificationProps() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.DisableAuditingUpdateModificationPropsProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldBeNull(); + douglas.LastModifierId.ShouldBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + + [Fact] + public async Task Should_Not_Set_Modification_If_Nested_ComplexProperties_HasDisableAuditing_UpdateModificationProps() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.DisableAuditingUpdateModificationPropsProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldBeNull(); + douglas.LastModifierId.ShouldBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + [Fact] public async Task Should_Not_PublishEntityEvent_If_Properties_HasDisableAuditing_PublishEntityEventProperty() { @@ -126,6 +198,48 @@ public class Auditing_Tests : Auditing_Tests EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any()); } + [Fact] + public async Task Should_Not_PublishEntityEvent_If_ComplexProperties_HasDisableAuditing_PublishEntityEventProperty() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.DisableAuditingPublishEntityEventProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any()); + } + + [Fact] + public async Task Should_Not_PublishEntityEvent_If_Nested_ComplexProperties_HasDisableAuditing_PublishEntityEventProperty() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.DisableAuditingPublishEntityEventProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.DidNotReceive().PublishEntityUpdatedEvent(Arg.Any()); + } + [Fact] public async Task Should_Set_Modification_And_PublishEntityEvent_If_Properties_HasDisableAuditing() @@ -146,4 +260,46 @@ public class Auditing_Tests : Auditing_Tests EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); } + + [Fact] + public async Task Should_Set_Modification_And_PublishEntityEvent_If_ComplexProperties_HasDisableAuditing() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.DisableAuditingProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } + + [Fact] + public async Task Should_Set_Modification_And_PublishEntityEvent_If_Nested_ComplexProperties_HasDisableAuditing() + { + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId); + douglas.ContactInformation ??= new PersonContactInformation(); + douglas.ContactInformation.Location.DisableAuditingProperty = Guid.NewGuid().ToString(); + })); + + await WithUnitOfWorkAsync((async () => + { + var douglas = await PersonRepository.FindAsync(TestDataBuilder.UserDouglasId); + + douglas.ShouldNotBeNull(); + douglas.LastModificationTime.ShouldNotBeNull(); + })); + + EntityChangeEventHelper.Received().PublishEntityUpdatedEvent(Arg.Any()); + } } 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 4c6efa5a02..96f4b1a2e1 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 @@ -78,6 +78,14 @@ public class TestMigrationsDbContext : AbpDbContext b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now); b.Property(x => x.TenantId).HasColumnName("Tenant_Id"); b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted"); + b.ComplexProperty(x => x.ContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, locationBuilder => + { + locationBuilder.Property(x => x.City).IsRequired(); + }); + }); }); modelBuilder.Entity(b => 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 a8e68dfca8..2b95877bed 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 @@ -92,6 +92,14 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.Property(x => x.HasDefaultValue).HasDefaultValue(DateTime.Now); b.Property(x => x.TenantId).HasColumnName("Tenant_Id"); b.Property(x => x.IsDeleted).HasColumnName("Is_Deleted"); + b.ComplexProperty(x => x.ContactInformation, cb => + { + cb.Property(x => x.Street).IsRequired(); + cb.ComplexProperty(x => x.Location, locationBuilder => + { + locationBuilder.Property(x => x.City).IsRequired(); + }); + }); }); modelBuilder diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index c05d0013d3..9da2357d70 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs @@ -32,6 +32,8 @@ public class Person : FullAuditedAggregateRoot, IMultiTenant, IHasEntityVe public virtual DateTime HasDefaultValue { get; set; } + public virtual PersonContactInformation? ContactInformation { get; set; } + public int EntityVersion { get; set; } [DisableAuditing(UpdateModificationProps = false)] @@ -84,3 +86,33 @@ public class Person : FullAuditedAggregateRoot, IMultiTenant, IHasEntityVe ); } } + +public class PersonContactInformation +{ + public string Street { get; set; } = string.Empty; + + public PersonContactLocation Location { get; set; } = new(); + + [DisableAuditing(UpdateModificationProps = false)] + public string? DisableAuditingUpdateModificationPropsProperty { get; set; } + + [DisableAuditing(PublishEntityEvent = false)] + public string? DisableAuditingPublishEntityEventProperty { get; set; } + + [DisableAuditing] + public string? DisableAuditingProperty { get; set; } +} + +public class PersonContactLocation +{ + public string City { get; set; } = string.Empty; + + [DisableAuditing(UpdateModificationProps = false)] + public string? DisableAuditingUpdateModificationPropsProperty { get; set; } + + [DisableAuditing(PublishEntityEvent = false)] + public string? DisableAuditingPublishEntityEventProperty { get; set; } + + [DisableAuditing] + public string? DisableAuditingProperty { get; set; } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs index de2e95895c..d46762615c 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs @@ -76,6 +76,10 @@ public class TestDataBuilder : ITransientDependency private async Task AddPeople() { var douglas = new Person(UserDouglasId, "Douglas", 42, cityId: LondonCityId); + douglas.ContactInformation = new PersonContactInformation + { + Street = "Test Street" + }; douglas.Phones.Add(new Phone(douglas.Id, "123456789")); douglas.Phones.Add(new Phone(douglas.Id, "123456780", PhoneType.Home));