From f253f9945e4be1b1c076aaefd5a5cbfb9fa2ab06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 15:19:43 +0300 Subject: [PATCH 1/7] Improve entity history handling for shared types and JSON Enhanced EntityHistoryHelper to correctly handle entities with shared CLR types and properties mapped to JSON. Added logic to determine property type from entry values when the type is object, and improved property change tracking for JSON-mapped navigation properties. --- .../EntityHistory/EntityHistoryHelper.cs | 55 ++++++++++++++++--- 1 file changed, 46 insertions(+), 9 deletions(-) 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 f87a05e4a9..c23eee6450 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 @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -108,12 +109,17 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } var entityType = entity.GetType(); + var entityFullName = entityType.FullName!; + if (entityEntry.Metadata.HasSharedClrType) + { + entityFullName = entityEntry.Metadata.Name; + } var entityChange = new EntityChangeInfo { ChangeType = changeType, EntityEntry = entityEntry, EntityId = entityId, - EntityTypeFullName = entityType.FullName, + EntityTypeFullName = entityFullName, PropertyChanges = GetPropertyChanges(entityEntry), EntityTenantId = GetTenantId(entity) }; @@ -184,12 +190,14 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency var propertyEntry = entityEntry.Property(property.Name); if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry)) { + var propertyType = DeterminePropertyTypeFromEntry(property, 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 = property.Name, - PropertyTypeFullName = property.ClrType.GetFirstGenericArgumentIfNullable().FullName! + PropertyTypeFullName = propertyType.FullName! }); } } @@ -208,14 +216,22 @@ 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 + if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) + { + var jsonPropertyChanges = GetPropertyChanges(referenceEntry.TargetEntry); + propertyChanges.AddRange(jsonPropertyChanges); + } + else { - PropertyName = navigationEntry.Metadata.Name, - PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, - OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), - NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) - }); + var isCollection = navigationEntry.Metadata.IsCollection; + propertyChanges.Add(new EntityPropertyChangeInfo + { + PropertyName = navigationEntry.Metadata.Name, + PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, + OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), + NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) + }); + } } } } @@ -223,6 +239,27 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) + { + var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); + + if (propertyType != typeof(object)) + { + return propertyType; + } + + if (propertyEntry.CurrentValue != null) + { + propertyType = propertyEntry.CurrentValue.GetType().GetFirstGenericArgumentIfNullable(); + } + else if (propertyEntry.OriginalValue != null) + { + propertyType = propertyEntry.OriginalValue.GetType().GetFirstGenericArgumentIfNullable(); + } + + return propertyType; + } + protected virtual string? GetNavigationPropertyValue(object? entity, bool isCollection) { switch (entity) From bdd0901dbad4618476f13642117a277c31317788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 15:41:38 +0300 Subject: [PATCH 2/7] Update framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../EntityHistory/EntityHistoryHelper.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 c23eee6450..7554c1d281 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 @@ -239,6 +239,18 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency return propertyChanges; } + /// + /// Determines the CLR type of a property based on its EF Core metadata and the values in the given . + /// + /// The EF Core property metadata that provides the declared CLR type. + /// The property entry that contains the current and original values for the property. + /// + /// The most specific CLR type inferred for the property. This is normally the property's declared CLR type (with + /// nullable wrappers removed). If the declared type is , the type is inferred from the + /// runtime type of or, if that is null, from + /// . If both values are null, the declared CLR type + /// (which may remain ) is returned. + /// protected virtual Type DeterminePropertyTypeFromEntry(IProperty property, PropertyEntry propertyEntry) { var propertyType = property.ClrType.GetFirstGenericArgumentIfNullable(); From d041aeb1fa2fc1b5dd231d32ab7d5ecd0c0e77d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 15:58:32 +0300 Subject: [PATCH 3/7] Improve JSON navigation property change tracking Refactored EntityHistoryHelper to better handle property changes for navigation properties mapped to JSON. Now, nested property changes are prefixed with the navigation property name, and duplicate property names are handled to avoid conflicts. --- .../EntityHistory/EntityHistoryHelper.cs | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) 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 7554c1d281..66e632dc8c 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 @@ -202,37 +202,47 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } - if (AbpEfCoreNavigationHelper != null) + if (AbpEfCoreNavigationHelper == null) { - foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) - { - var propertyInfo = navigationEntry.Metadata.PropertyInfo; - if (propertyInfo != null && - propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) - { - continue; - } + return propertyChanges; + } - if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) + foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) + { + var propertyInfo = navigationEntry.Metadata.PropertyInfo; + if (propertyInfo != null && + propertyInfo.IsDefined(typeof(DisableAuditingAttribute), true)) + { + continue; + } + + if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) + { + foreach (var propertyChange in GetPropertyChanges(referenceEntry.TargetEntry)) { - var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index); - if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) - { - var jsonPropertyChanges = GetPropertyChanges(referenceEntry.TargetEntry); - propertyChanges.AddRange(jsonPropertyChanges); - } - else + if (propertyChanges.Any(pc => pc.PropertyName == propertyChange.PropertyName)) { - var isCollection = navigationEntry.Metadata.IsCollection; - propertyChanges.Add(new EntityPropertyChangeInfo - { - PropertyName = navigationEntry.Metadata.Name, - PropertyTypeFullName = navigationEntry.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName!, - OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), - NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) - }); + propertyChange.PropertyName = $"{referenceEntry.Metadata.Name}.{propertyChange.PropertyName}"; } + + propertyChanges.Add(propertyChange); } + + continue; + } + + 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!, + OriginalValue = GetNavigationPropertyValue(abpNavigationEntry?.OriginalValue, isCollection), + NewValue = GetNavigationPropertyValue(abpNavigationEntry?.CurrentValue, isCollection) + }); } } From fb5dbaee99ccd1b7a81c7952559e70102ee9cc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Tue, 30 Dec 2025 19:02:53 +0300 Subject: [PATCH 4/7] Fix entity type name for shared CLR types Update EntityHistoryHelper to set the entity type name only for shared CLR types that are not owned entities. This prevents incorrect entity name assignment for owned types. --- .../EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 66e632dc8c..2fcdb35480 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 @@ -110,7 +110,7 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency var entityType = entity.GetType(); var entityFullName = entityType.FullName!; - if (entityEntry.Metadata.HasSharedClrType) + if (entityEntry.Metadata.HasSharedClrType && !entityEntry.Metadata.IsOwned()) { entityFullName = entityEntry.Metadata.Name; } From 4d00ee9365dd8fd09716caaf86c6371c660ce61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?SAL=C4=B0H=20=C3=96ZKARA?= Date: Wed, 31 Dec 2025 11:40:14 +0300 Subject: [PATCH 5/7] Add support and tests for entity history with JSON properties Introduces AppEntityWithJsonProperty and related DbSet to test contexts, configures model to handle owned JSON properties, and adds tests to verify entity history tracking for nested JSON property changes and shared entities. Refactors EntityHistoryHelper to improve navigation property change handling. --- .../EntityHistory/EntityHistoryHelper.cs | 10 +- .../AbpEntityFrameworkCoreTestModule.cs | 7 + .../Auditing/EntityHistoryHelper_Tests.cs | 130 ++++++++++++++++++ .../TestMigrationsDbContext.cs | 23 ++++ .../EntityFrameworkCore/TestAppDbContext.cs | 25 +++- .../Domain/AppEntityWithJsonProperty.cs | 28 ++++ 6 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs 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 2fcdb35480..e659689da1 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 @@ -202,11 +202,6 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } - if (AbpEfCoreNavigationHelper == null) - { - return propertyChanges; - } - foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) { var propertyInfo = navigationEntry.Metadata.PropertyInfo; @@ -230,6 +225,11 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency continue; } + + if (AbpEfCoreNavigationHelper == null) + { + return propertyChanges; + } if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index a7b5c1e602..e4183a5a19 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Auditing; using Volo.Abp.Autofac; using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.Domain; @@ -86,6 +87,12 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection).AddAbpDbContextOptionsExtension(); }); }); + + Configure(options => + { + options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty))); + options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(TestSharedEntity), type => type == typeof(TestSharedEntity))); + }); } public override void OnPreApplicationInitialization(ApplicationInitializationContext context) diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs new file mode 100644 index 0000000000..65873dd514 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Auditing; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.EntityFrameworkCore.EntityHistory; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Auditing; + +public class EntityHistoryHelper_Tests : EntityFrameworkCoreTestBase +{ + private readonly IEntityHistoryHelper _entityHistoryHelper; + private readonly IRepository _appEntityWithJsonRepository; + private readonly IRepository _testSharedEntityRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public EntityHistoryHelper_Tests() + { + _entityHistoryHelper = GetRequiredService(); + _appEntityWithJsonRepository = GetRequiredService>(); + _testSharedEntityRepository = GetRequiredService>(); + _unitOfWorkManager = GetRequiredService(); + } + + [Fact] + public async Task CreateChangeList_Should_Track_Nested_Json_Property_Changes_As_Separate_Property_Changes() + { + // Arrange & Act + EntityChangeInfo entityChange = null; + + await WithUnitOfWorkAsync(async () => + { + var entity = new AppEntityWithJsonProperty(Guid.NewGuid(), "Test Entity") + { + Data = new JsonPropertyObject() + { + { "Name", "String Name" }, + { "Value", "String Value"} + }, + Count = 10 + }; + + await _appEntityWithJsonRepository.InsertAsync(entity); + + var dbContext = await GetDbContextAsync(); + + var entries = dbContext.ChangeTracker.Entries().ToList(); + var entityChanges = _entityHistoryHelper.CreateChangeList(entries); + + entityChange = entityChanges.FirstOrDefault(x => x.EntityTypeFullName.Contains(nameof(AppEntityWithJsonProperty))); + }); + + // Assert + entityChange.ShouldNotBeNull(); + var dataPropertyChange = entityChange.PropertyChanges.FirstOrDefault(x => x.PropertyName == nameof(AppEntityWithJsonProperty.Data)); + dataPropertyChange.ShouldBeNull(); + var jsonNamePropertyChange = entityChange.PropertyChanges.FirstOrDefault(x => x.PropertyName == nameof(AppEntityWithJsonProperty.Data) + "." + "Name"); + jsonNamePropertyChange.ShouldNotBeNull(); + jsonNamePropertyChange.PropertyTypeFullName.ShouldBe(typeof(string).FullName); + jsonNamePropertyChange.NewValue.ShouldBe("\"String Name\""); + + var jsonValuePropertyChange = entityChange.PropertyChanges.FirstOrDefault(x => x.PropertyName == "Value"); + jsonValuePropertyChange.ShouldNotBeNull(); + jsonValuePropertyChange.PropertyTypeFullName.ShouldBe(typeof(string).FullName); + jsonValuePropertyChange.NewValue.ShouldBe("\"String Value\""); + } + + [Fact] + public async Task CreateChangeList_Should_Track_Shared_Entities_With_Their_Respective_Entity_Names() + { + // Arrange & Act + List entityChanges = null; + + await WithUnitOfWorkAsync(async () => + { + var entity = new TestSharedEntity(Guid.NewGuid()) + { + TenantId = null, + IsDeleted = false, + Name = "Test Person1", + Age = 10, + Birthday = DateTime.Now + }.SetProperty("testProperty", "Test Value1"); + + _testSharedEntityRepository.SetEntityName("TestSharedEntity1"); + await _testSharedEntityRepository.InsertAsync(entity); + + var entity2 = new TestSharedEntity(Guid.NewGuid()) + { + TenantId = null, + IsDeleted = false, + Name = "Test Person2", + Age = 20, + Birthday = DateTime.Now + }.SetProperty("testProperty", "Test Value2"); + + _testSharedEntityRepository.SetEntityName("TestSharedEntity2"); + await _testSharedEntityRepository.InsertAsync(entity2); + + var dbContext = await GetDbContextAsync(); + + var entries = dbContext.ChangeTracker.Entries().ToList(); + entityChanges = _entityHistoryHelper.CreateChangeList(entries); + }); + + entityChanges.ShouldContain(x => x.EntityTypeFullName == "TestSharedEntity1"); + entityChanges.ShouldContain(x => x.EntityTypeFullName == "TestSharedEntity2"); + } + + private async Task GetDbContextAsync() + { + var uow = _unitOfWorkManager.Current; + if (uow == null) + { + throw new InvalidOperationException("No active unit of work found"); + } + + var dbContextProvider = uow.ServiceProvider.GetRequiredService>(); + return await dbContextProvider.GetDbContextAsync(); + } +} + 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..6e102706fc 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 @@ -37,6 +37,8 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet TestSharedEntity => Set("TestSharedEntity1"); public DbSet TestSharedEntity2 => Set("TestSharedEntity2"); + + public DbSet EntitiesWithObjectProperty { get; set; } public TestMigrationsDbContext(DbContextOptions options) : base(options) @@ -140,5 +142,26 @@ public class TestMigrationsDbContext : AbpDbContext { b.ConfigureByConvention(); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.OwnsOne(x => x.Data, b2 => + { + b2.ToJson(); + + b2.Property("Name") + .HasConversion( + v => v.ToString(), + v => v + ); + + b2.Property("Value") + .HasConversion( + v => v.ToString(), + v => v + ); + }); + }); } } 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..b0b0a55c03 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 @@ -40,9 +40,11 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Blogs { get; set; } public DbSet BlogPosts { get; set; } - + public DbSet TestSharedEntity => Set("TestSharedEntity1"); public DbSet TestSharedEntity2 => Set("TestSharedEntity2"); + + public DbSet EntitiesWithObjectProperty { get; set; } public TestAppDbContext(DbContextOptions options) : base(options) @@ -166,6 +168,27 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.ConfigureByConvention(); }); + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.OwnsOne(x => x.Data, b2 => + { + b2.ToJson(); + + b2.Property("Name") + .HasConversion( + v => v.ToString(), + v => v + ); + + b2.Property("Value") + .HasConversion( + v => v.ToString(), + v => v + ); + }); + }); + modelBuilder.TryConfigureObjectExtensions(); } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs new file mode 100644 index 0000000000..ab830e3e85 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Volo.Abp.TestApp.Domain; + +public class AppEntityWithJsonProperty : FullAuditedAggregateRoot +{ + public string Name { get; set; } + + public JsonPropertyObject Data { get; set; } + + public int Count { get; set; } + + public AppEntityWithJsonProperty() + { + } + + public AppEntityWithJsonProperty(Guid id, string name) : base(id) + { + Name = name; + } +} + +public class JsonPropertyObject : Dictionary +{ +} + From 96a7303fc6a73eea9eb81f1e01040c68ad5c628d Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 31 Dec 2025 18:17:36 +0800 Subject: [PATCH 6/7] Revert "Add support and tests for entity history with JSON properties" This reverts commit 4d00ee9365dd8fd09716caaf86c6371c660ce61b. --- .../EntityHistory/EntityHistoryHelper.cs | 10 +- .../AbpEntityFrameworkCoreTestModule.cs | 7 - .../Auditing/EntityHistoryHelper_Tests.cs | 130 ------------------ .../TestMigrationsDbContext.cs | 23 ---- .../EntityFrameworkCore/TestAppDbContext.cs | 25 +--- .../Domain/AppEntityWithJsonProperty.cs | 28 ---- 6 files changed, 6 insertions(+), 217 deletions(-) delete mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs delete mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs 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 e659689da1..2fcdb35480 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 @@ -202,6 +202,11 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } + if (AbpEfCoreNavigationHelper == null) + { + return propertyChanges; + } + foreach (var (navigationEntry, index) in entityEntry.Navigations.Select((value, i) => ( value, i ))) { var propertyInfo = navigationEntry.Metadata.PropertyInfo; @@ -225,11 +230,6 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency continue; } - - if (AbpEfCoreNavigationHelper == null) - { - return propertyChanges; - } if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs index e4183a5a19..a7b5c1e602 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/AbpEntityFrameworkCoreTestModule.cs @@ -5,7 +5,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Auditing; using Volo.Abp.Autofac; using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore.Domain; @@ -87,12 +86,6 @@ public class AbpEntityFrameworkCoreTestModule : AbpModule abpDbContextConfigurationContext.DbContextOptions.UseSqlite(sqliteConnection).AddAbpDbContextOptionsExtension(); }); }); - - Configure(options => - { - options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty))); - options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(TestSharedEntity), type => type == typeof(TestSharedEntity))); - }); } public override void OnPreApplicationInitialization(ApplicationInitializationContext context) diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs deleted file mode 100644 index 65873dd514..0000000000 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Auditing/EntityHistoryHelper_Tests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Shouldly; -using Volo.Abp.Auditing; -using Volo.Abp.Data; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.EntityFrameworkCore.EntityHistory; -using Volo.Abp.TestApp.Domain; -using Volo.Abp.TestApp.EntityFrameworkCore; -using Volo.Abp.Uow; -using Xunit; - -namespace Volo.Abp.EntityFrameworkCore.Auditing; - -public class EntityHistoryHelper_Tests : EntityFrameworkCoreTestBase -{ - private readonly IEntityHistoryHelper _entityHistoryHelper; - private readonly IRepository _appEntityWithJsonRepository; - private readonly IRepository _testSharedEntityRepository; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - public EntityHistoryHelper_Tests() - { - _entityHistoryHelper = GetRequiredService(); - _appEntityWithJsonRepository = GetRequiredService>(); - _testSharedEntityRepository = GetRequiredService>(); - _unitOfWorkManager = GetRequiredService(); - } - - [Fact] - public async Task CreateChangeList_Should_Track_Nested_Json_Property_Changes_As_Separate_Property_Changes() - { - // Arrange & Act - EntityChangeInfo entityChange = null; - - await WithUnitOfWorkAsync(async () => - { - var entity = new AppEntityWithJsonProperty(Guid.NewGuid(), "Test Entity") - { - Data = new JsonPropertyObject() - { - { "Name", "String Name" }, - { "Value", "String Value"} - }, - Count = 10 - }; - - await _appEntityWithJsonRepository.InsertAsync(entity); - - var dbContext = await GetDbContextAsync(); - - var entries = dbContext.ChangeTracker.Entries().ToList(); - var entityChanges = _entityHistoryHelper.CreateChangeList(entries); - - entityChange = entityChanges.FirstOrDefault(x => x.EntityTypeFullName.Contains(nameof(AppEntityWithJsonProperty))); - }); - - // Assert - entityChange.ShouldNotBeNull(); - var dataPropertyChange = entityChange.PropertyChanges.FirstOrDefault(x => x.PropertyName == nameof(AppEntityWithJsonProperty.Data)); - dataPropertyChange.ShouldBeNull(); - var jsonNamePropertyChange = entityChange.PropertyChanges.FirstOrDefault(x => x.PropertyName == nameof(AppEntityWithJsonProperty.Data) + "." + "Name"); - jsonNamePropertyChange.ShouldNotBeNull(); - jsonNamePropertyChange.PropertyTypeFullName.ShouldBe(typeof(string).FullName); - jsonNamePropertyChange.NewValue.ShouldBe("\"String Name\""); - - var jsonValuePropertyChange = entityChange.PropertyChanges.FirstOrDefault(x => x.PropertyName == "Value"); - jsonValuePropertyChange.ShouldNotBeNull(); - jsonValuePropertyChange.PropertyTypeFullName.ShouldBe(typeof(string).FullName); - jsonValuePropertyChange.NewValue.ShouldBe("\"String Value\""); - } - - [Fact] - public async Task CreateChangeList_Should_Track_Shared_Entities_With_Their_Respective_Entity_Names() - { - // Arrange & Act - List entityChanges = null; - - await WithUnitOfWorkAsync(async () => - { - var entity = new TestSharedEntity(Guid.NewGuid()) - { - TenantId = null, - IsDeleted = false, - Name = "Test Person1", - Age = 10, - Birthday = DateTime.Now - }.SetProperty("testProperty", "Test Value1"); - - _testSharedEntityRepository.SetEntityName("TestSharedEntity1"); - await _testSharedEntityRepository.InsertAsync(entity); - - var entity2 = new TestSharedEntity(Guid.NewGuid()) - { - TenantId = null, - IsDeleted = false, - Name = "Test Person2", - Age = 20, - Birthday = DateTime.Now - }.SetProperty("testProperty", "Test Value2"); - - _testSharedEntityRepository.SetEntityName("TestSharedEntity2"); - await _testSharedEntityRepository.InsertAsync(entity2); - - var dbContext = await GetDbContextAsync(); - - var entries = dbContext.ChangeTracker.Entries().ToList(); - entityChanges = _entityHistoryHelper.CreateChangeList(entries); - }); - - entityChanges.ShouldContain(x => x.EntityTypeFullName == "TestSharedEntity1"); - entityChanges.ShouldContain(x => x.EntityTypeFullName == "TestSharedEntity2"); - } - - private async Task GetDbContextAsync() - { - var uow = _unitOfWorkManager.Current; - if (uow == null) - { - throw new InvalidOperationException("No active unit of work found"); - } - - var dbContextProvider = uow.ServiceProvider.GetRequiredService>(); - return await dbContextProvider.GetDbContextAsync(); - } -} - 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 6e102706fc..4c6efa5a02 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 @@ -37,8 +37,6 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet TestSharedEntity => Set("TestSharedEntity1"); public DbSet TestSharedEntity2 => Set("TestSharedEntity2"); - - public DbSet EntitiesWithObjectProperty { get; set; } public TestMigrationsDbContext(DbContextOptions options) : base(options) @@ -142,26 +140,5 @@ public class TestMigrationsDbContext : AbpDbContext { b.ConfigureByConvention(); }); - - modelBuilder.Entity(b => - { - b.ConfigureByConvention(); - b.OwnsOne(x => x.Data, b2 => - { - b2.ToJson(); - - b2.Property("Name") - .HasConversion( - v => v.ToString(), - v => v - ); - - b2.Property("Value") - .HasConversion( - v => v.ToString(), - v => v - ); - }); - }); } } 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 b0b0a55c03..a8e68dfca8 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 @@ -40,11 +40,9 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Blogs { get; set; } public DbSet BlogPosts { get; set; } - + public DbSet TestSharedEntity => Set("TestSharedEntity1"); public DbSet TestSharedEntity2 => Set("TestSharedEntity2"); - - public DbSet EntitiesWithObjectProperty { get; set; } public TestAppDbContext(DbContextOptions options) : base(options) @@ -168,27 +166,6 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.ConfigureByConvention(); }); - modelBuilder.Entity(b => - { - b.ConfigureByConvention(); - b.OwnsOne(x => x.Data, b2 => - { - b2.ToJson(); - - b2.Property("Name") - .HasConversion( - v => v.ToString(), - v => v - ); - - b2.Property("Value") - .HasConversion( - v => v.ToString(), - v => v - ); - }); - }); - modelBuilder.TryConfigureObjectExtensions(); } } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs deleted file mode 100644 index ab830e3e85..0000000000 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/AppEntityWithJsonProperty.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Volo.Abp.TestApp.Domain; - -public class AppEntityWithJsonProperty : FullAuditedAggregateRoot -{ - public string Name { get; set; } - - public JsonPropertyObject Data { get; set; } - - public int Count { get; set; } - - public AppEntityWithJsonProperty() - { - } - - public AppEntityWithJsonProperty(Guid id, string name) : base(id) - { - Name = name; - } -} - -public class JsonPropertyObject : Dictionary -{ -} - From 6f57ec45680bef671f13d0416c789f8b8def6f65 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 31 Dec 2025 19:11:25 +0800 Subject: [PATCH 7/7] Add audit support for JSON property changes. --- .../EntityHistory/EntityHistoryHelper.cs | 17 ++-- .../Abp/Auditing/AbpAuditingTestModule.cs | 2 + .../App/Entities/AppEntityWithJsonProperty.cs | 27 +++++ .../AbpAuditingTestDbContext.cs | 21 ++++ .../Volo/Abp/Auditing/Auditing_Tests.cs | 98 +++++++++++++++++++ 5 files changed, 157 insertions(+), 8 deletions(-) create mode 100644 framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithJsonProperty.cs 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 2fcdb35480..75e82976b4 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 @@ -187,6 +187,11 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency foreach (var property in properties) { + if (entityEntry.Metadata.IsMappedToJson() && property.GetJsonPropertyName() == null) + { + continue; + } + var propertyEntry = entityEntry.Property(property.Name); if (ShouldSavePropertyHistory(propertyEntry, isCreated || isDeleted) && !IsSoftDeleted(entityEntry)) { @@ -215,26 +220,22 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency { continue; } - + if (navigationEntry.Metadata.TargetEntityType.IsMappedToJson() && navigationEntry is ReferenceEntry referenceEntry && referenceEntry.TargetEntry != null) { foreach (var propertyChange in GetPropertyChanges(referenceEntry.TargetEntry)) { - if (propertyChanges.Any(pc => pc.PropertyName == propertyChange.PropertyName)) - { - propertyChange.PropertyName = $"{referenceEntry.Metadata.Name}.{propertyChange.PropertyName}"; - } - + propertyChange.PropertyName = $"{referenceEntry.Metadata.Name}.{propertyChange.PropertyName}"; propertyChanges.Add(propertyChange); } - + continue; } if (AbpEfCoreNavigationHelper.IsNavigationEntryModified(entityEntry, index)) { var abpNavigationEntry = AbpEfCoreNavigationHelper.GetNavigationEntry(entityEntry, index); - + var isCollection = navigationEntry.Metadata.IsCollection; propertyChanges.Add(new EntityPropertyChangeInfo { 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 4e14dbc262..800b73544d 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 @@ -59,6 +59,8 @@ public class AbpAuditingTestModule : AbpModule "AppEntityWithValueObject", type => type == typeof(AppEntityWithValueObject) || type == typeof(AppEntityWithValueObjectAddress)) ); + + options.EntityHistorySelectors.Add(new NamedTypeSelector(nameof(AppEntityWithJsonProperty), type => type == typeof(AppEntityWithJsonProperty))); }); context.Services.AddType(); diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithJsonProperty.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithJsonProperty.cs new file mode 100644 index 0000000000..de07544629 --- /dev/null +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithJsonProperty.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Volo.Abp.Auditing.App.Entities; + +public class AppEntityWithJsonProperty : FullAuditedAggregateRoot +{ + public string Name { get; set; } + + public JsonPropertyObject Data { get; set; } + + public int Count { get; set; } + + public AppEntityWithJsonProperty() + { + } + + public AppEntityWithJsonProperty(Guid id, string name) : base(id) + { + Name = name; + } +} + +public class JsonPropertyObject : Dictionary +{ +} 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 91698ea5ec..e8950880d7 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 @@ -30,6 +30,7 @@ public class AbpAuditingTestDbContext : AbpDbContext public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } public DbSet AppEntityWithNavigationsAndDisableAuditing { get; set; } + public DbSet EntitiesWithObjectProperty { get; set; } public AbpAuditingTestDbContext(DbContextOptions options) : base(options) @@ -56,5 +57,25 @@ public class AbpAuditingTestDbContext : AbpDbContext b.HasMany(x => x.ManyToMany).WithMany(x => x.ManyToMany).UsingEntity(); }); + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.OwnsOne(x => x.Data, b2 => + { + b2.ToJson(); + + b2.Property("Name") + .HasConversion( + v => v.ToString(), + v => v + ); + + b2.Property("Value") + .HasConversion( + v => v.ToString(), + v => v + ); + }); + }); } } 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 a88cf70292..637b9b4d97 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 @@ -720,6 +720,104 @@ public class Auditing_Tests : AbpAuditingTestBase x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigationChildManyToMany.ManyToMany) && x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); +#pragma warning restore 4014 + } + + [Fact] + public async Task Should_Write_AuditLog_For_Json_Property_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = new AppEntityWithJsonProperty(entityId, "Test Entity") + { + Data = new JsonPropertyObject() + { + { "Name", "String Name" }, + { "Value", "String Value"} + }, + Count = 10 + }; + + 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(AppEntityWithJsonProperty).FullName && + x.EntityChanges[0].PropertyChanges.Count == 4 && + + x.EntityChanges[0].PropertyChanges[0].OriginalValue == null && + x.EntityChanges[0].PropertyChanges[0].NewValue == "10" && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithJsonProperty.Count) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(int).FullName && + + x.EntityChanges[0].PropertyChanges[1].OriginalValue == null && + x.EntityChanges[0].PropertyChanges[1].NewValue == "\"Test Entity\"" && + x.EntityChanges[0].PropertyChanges[1].PropertyName == nameof(AppEntityWithJsonProperty.Name) && + x.EntityChanges[0].PropertyChanges[1].PropertyTypeFullName == typeof(string).FullName && + + x.EntityChanges[0].PropertyChanges[2].OriginalValue == null && + x.EntityChanges[0].PropertyChanges[2].NewValue == "\"String Name\"" && + x.EntityChanges[0].PropertyChanges[2].PropertyName == "Data.Name" && + x.EntityChanges[0].PropertyChanges[2].PropertyTypeFullName == typeof(string).FullName && + + x.EntityChanges[0].PropertyChanges[3].OriginalValue == null && + x.EntityChanges[0].PropertyChanges[3].NewValue == "\"String Value\"" && + x.EntityChanges[0].PropertyChanges[3].PropertyName == "Data.Value" && + x.EntityChanges[0].PropertyChanges[3].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.Name = "Updated Test Entity"; + + entity.Data["Name"] = "Updated String Name"; + entity.Data["Value"] = "Updated String Value"; + + 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(AppEntityWithJsonProperty).FullName && + x.EntityChanges[0].PropertyChanges.Count == 3 && + + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"Test Entity\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Updated Test Entity\"" && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithJsonProperty.Name) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName && + + x.EntityChanges[0].PropertyChanges[1].OriginalValue == "\"String Name\"" && + x.EntityChanges[0].PropertyChanges[1].NewValue == "\"Updated String Name\"" && + x.EntityChanges[0].PropertyChanges[1].PropertyName == "Data.Name" && + x.EntityChanges[0].PropertyChanges[1].PropertyTypeFullName == typeof(string).FullName && + + x.EntityChanges[0].PropertyChanges[2].OriginalValue == "\"String Value\"" && + x.EntityChanges[0].PropertyChanges[2].NewValue == "\"Updated String Value\"" && + x.EntityChanges[0].PropertyChanges[2].PropertyName == "Data.Value" && + x.EntityChanges[0].PropertyChanges[2].PropertyTypeFullName == typeof(string).FullName)); + AuditingStore.ClearReceivedCalls(); #pragma warning restore 4014 } }