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] 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 +{ +} +