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