From ae936150e662ca2d61b17520ae9c934b46b4d3ff Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 24 Sep 2026 10:50:57 +0800 Subject: [PATCH 1/3] Handle duplicate setting, feature and permission records --- .../FeatureManagementStore.cs | 62 ++++++++++++- .../FeatureManagementStore_Tests.cs | 48 ++++++++++ .../PermissionManagementProvider.cs | 8 +- .../ResourcePermissionManagementProvider.cs | 8 +- .../PermissionManagementProvider_Tests.cs | 15 ++++ ...ourcePermissionManagementProvider_Tests.cs | 35 ++++++++ .../SettingManagementStore.cs | 82 +++++++++++++++-- .../DuplicateSettingRecords_Tests.cs | 6 ++ .../MongoDB/DuplicateSettingRecords_Tests.cs | 9 ++ .../DuplicateSettingRecords_Tests.cs | 89 +++++++++++++++++++ 10 files changed, 340 insertions(+), 22 deletions(-) create mode 100644 modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/DuplicateSettingRecords_Tests.cs create mode 100644 modules/setting-management/test/Volo.Abp.SettingManagement.MongoDB.Tests/Volo/Abp/SettingManagement/MongoDB/DuplicateSettingRecords_Tests.cs create mode 100644 modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/DuplicateSettingRecords_Tests.cs diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs index d61738afb29..34ca65eb6a4 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Features; @@ -11,6 +13,8 @@ namespace Volo.Abp.FeatureManagement; public class FeatureManagementStore : IFeatureManagementStore, ITransientDependency { + public ILogger Logger { get; set; } + protected IDistributedCache Cache { get; } protected IFeatureDefinitionManager FeatureDefinitionManager { get; } protected IFeatureValueRepository FeatureValueRepository { get; } @@ -26,6 +30,7 @@ public FeatureManagementStore( GuidGenerator = guidGenerator; Cache = cache; FeatureDefinitionManager = featureDefinitionManager; + Logger = NullLogger.Instance; } [UnitOfWork] @@ -38,7 +43,7 @@ public virtual async Task GetOrNullAsync(string name, string providerNam [UnitOfWork] public virtual async Task SetAsync(string name, string value, string providerName, string providerKey) { - var featureValue = await FeatureValueRepository.FindAsync(name, providerName, providerKey); + var featureValue = await FindAndDeleteDuplicatesAsync(name, providerName, providerKey); if (featureValue == null) { featureValue = new FeatureValue(GuidGenerator.Create(), name, value, providerName, providerKey); @@ -88,8 +93,10 @@ private async Task SetCacheItemsAsync( FeatureValueCacheItem currentCacheItem) { var featureDefinitions = await FeatureDefinitionManager.GetAllAsync(); - var featuresDictionary = (await FeatureValueRepository.GetListAsync(providerName, providerKey)) - .ToDictionary(s => s.Name, s => s.Value); + var featuresDictionary = await CreateFeatureValueDictionaryAsync( + await FeatureValueRepository.GetListAsync(providerName, providerKey), + providerName, + providerKey); var cacheItems = new List>(); @@ -113,6 +120,55 @@ private async Task SetCacheItemsAsync( await Cache.SetManyAsync(cacheItems, considerUow: true); } + protected virtual async Task FindAndDeleteDuplicatesAsync(string name, string providerName, string providerKey) + { + var featureValues = await FeatureValueRepository.FindAllAsync(name, providerName, providerKey); + if (featureValues.Count <= 1) + { + return featureValues.FirstOrDefault(); + } + + var featureValue = await FeatureValueRepository.FindAsync(name, providerName, providerKey); + if (featureValue == null) + { + return null; + } + + foreach (var duplicate in featureValues.Where(x => x.Id != featureValue.Id)) + { + await FeatureValueRepository.DeleteAsync(duplicate, true); + } + + return featureValue; + } + + protected virtual async Task> CreateFeatureValueDictionaryAsync( + List featureValues, + string providerName, + string providerKey) + { + var featuresDictionary = new Dictionary(); + + foreach (var featureValueGroup in featureValues.GroupBy(s => s.Name)) + { + if (featureValueGroup.Count() == 1) + { + featuresDictionary[featureValueGroup.Key] = featureValueGroup.First().Value; + continue; + } + + // FindAsync returns the same record that SetAsync updates, so reads and writes agree until the duplicates are deleted. + Logger.LogWarning( + "Found {Count} feature value records for Name = {Name}, ProviderName = {ProviderName}, ProviderKey = {ProviderKey}. The duplicates will be deleted the next time the feature value is saved.", + featureValueGroup.Count(), featureValueGroup.Key, providerName, providerKey); + + var featureValue = await FeatureValueRepository.FindAsync(featureValueGroup.Key, providerName, providerKey); + featuresDictionary[featureValueGroup.Key] = (featureValue ?? featureValueGroup.First()).Value; + } + + return featuresDictionary; + } + protected virtual string CalculateCacheKey(string name, string providerName, string providerKey) { return FeatureValueCacheItem.CalculateCacheKey(name, providerName, providerKey); diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs index bec82eaadaa..c18067506fb 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.TestBase/Volo/Abp/FeatureManagement/FeatureManagementStore_Tests.cs @@ -141,4 +141,52 @@ await FeatureManagementStore.DeleteAsync(TestFeatureDefinitionProvider.SocialLog TestEditionIds.Regular.ToString())).ShouldBeNull(); } } + + [Fact] + public async Task GetOrNullAsync_Should_Not_Fail_When_Duplicate_Host_Values_Exist() + { + // Arrange + await InsertDuplicateHostFeatureValuesAsync(TestFeatureDefinitionProvider.SocialLogins); + + // Act & Assert + (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.UserCount, + TenantFeatureValueProvider.ProviderName, + null)).ShouldBeNull(); + + (await FeatureManagementStore.GetOrNullAsync(TestFeatureDefinitionProvider.SocialLogins, + TenantFeatureValueProvider.ProviderName, + null)).ShouldBe((await FeatureValueRepository.FindAsync(TestFeatureDefinitionProvider.SocialLogins, + TenantFeatureValueProvider.ProviderName, + null)).Value); + } + + [Fact] + public async Task SetAsync_Should_Remove_Duplicate_Host_Values() + { + // Arrange + await InsertDuplicateHostFeatureValuesAsync(TestFeatureDefinitionProvider.SocialLogins); + + // Act + await FeatureManagementStore.SetAsync(TestFeatureDefinitionProvider.SocialLogins, + false.ToString().ToUpperInvariant(), + TenantFeatureValueProvider.ProviderName, + null); + + // Assert + var featureValues = await FeatureValueRepository.FindAllAsync(TestFeatureDefinitionProvider.SocialLogins, + TenantFeatureValueProvider.ProviderName, + null); + featureValues.Count.ShouldBe(1); + featureValues[0].Value.ShouldBe(false.ToString().ToUpperInvariant()); + } + + private async Task InsertDuplicateHostFeatureValuesAsync(string name) + { + await FeatureValueRepository.InsertAsync(new FeatureValue(Guid.NewGuid(), name, + true.ToString().ToLowerInvariant(), TenantFeatureValueProvider.ProviderName, null)); + await FeatureValueRepository.InsertAsync(new FeatureValue(Guid.NewGuid(), name, + false.ToString().ToLowerInvariant(), TenantFeatureValueProvider.ProviderName, null)); + + (await FeatureValueRepository.FindAllAsync(name, TenantFeatureValueProvider.ProviderName, null)).Count.ShouldBe(2); + } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs index b96fed734e9..bbff5e49feb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionManagementProvider.cs @@ -78,12 +78,10 @@ protected virtual async Task GrantAsync(string name, string providerKey) protected virtual async Task RevokeAsync(string name, string providerKey) { - var permissionGrant = await PermissionGrantRepository.FindAsync(name, Name, providerKey); - if (permissionGrant == null) + var permissionGrants = await PermissionGrantRepository.GetListAsync(new[] { name }, Name, providerKey); + foreach (var permissionGrant in permissionGrants) { - return; + await PermissionGrantRepository.DeleteAsync(permissionGrant, true); } - - await PermissionGrantRepository.DeleteAsync(permissionGrant, true); } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs index c579307c867..af9576ea114 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider.cs @@ -81,12 +81,10 @@ protected virtual async Task GrantAsync(string name, string resourceName, string protected virtual async Task RevokeAsync(string name, string resourceName,string resourceKey, string providerKey) { - var resourcePermissionGrants = await ResourcePermissionGrantRepository.FindAsync(name, resourceName, resourceKey, Name, providerKey); - if (resourcePermissionGrants == null) + var resourcePermissionGrants = await ResourcePermissionGrantRepository.GetListAsync(new[] { name }, resourceName, resourceKey, Name, providerKey); + foreach (var resourcePermissionGrant in resourcePermissionGrants) { - return; + await ResourcePermissionGrantRepository.DeleteAsync(resourcePermissionGrant, true); } - - await ResourcePermissionGrantRepository.DeleteAsync(resourcePermissionGrants, true); } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManagementProvider_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManagementProvider_Tests.cs index 8ea3ebf6722..68629f4309d 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManagementProvider_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/PermissionManagementProvider_Tests.cs @@ -75,4 +75,19 @@ await _permissionManagementProvider.SetAsync("MyPermission1", "Test")).ShouldBeNull(); } + [Fact] + public async Task SetAsync_Should_Revoke_Duplicate_Host_Grants() + { + await _permissionGrantRepository.InsertAsync(new PermissionGrant(Guid.NewGuid(), "MyPermission1", "Test", "Test")); + await _permissionGrantRepository.InsertAsync(new PermissionGrant(Guid.NewGuid(), "MyPermission1", "Test", "Test")); + (await _permissionGrantRepository.GetListAsync(new[] { "MyPermission1" }, "Test", "Test")).Count.ShouldBe(2); + + await _permissionManagementProvider.SetAsync("MyPermission1", + "Test", + false); + + (await _permissionGrantRepository.FindAsync("MyPermission1", + "Test", + "Test")).ShouldBeNull(); + } } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider_Tests.cs index 9651c3f3138..4f6a4318a94 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManagementProvider_Tests.cs @@ -87,4 +87,39 @@ await _resourcePermissionManagementProvider.SetAsync("MyPermission1", "Test", "Test")).ShouldBeNull(); } + + [Fact] + public async Task SetAsync_Should_Revoke_Duplicate_Host_Grants() + { + for (var i = 0; i < 2; i++) + { + await _resourcePermissionGrantRepository.InsertAsync( + new ResourcePermissionGrant( + Guid.NewGuid(), + "MyPermission1", + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test" + ) + ); + } + (await _resourcePermissionGrantRepository.GetListAsync(new[] { "MyPermission1" }, + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test")).Count.ShouldBe(2); + + await _resourcePermissionManagementProvider.SetAsync("MyPermission1", + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + false); + + (await _resourcePermissionGrantRepository.FindAsync("MyPermission1", + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test")).ShouldBeNull(); + } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs index 09f90c89195..3a0ff8100a0 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Caching; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; @@ -11,6 +13,8 @@ namespace Volo.Abp.SettingManagement; public class SettingManagementStore : ISettingManagementStore, ITransientDependency { + public ILogger Logger { get; set; } + protected IDistributedCache Cache { get; } protected ISettingDefinitionManager SettingDefinitionManager { get; } protected ISettingRepository SettingRepository { get; } @@ -26,6 +30,7 @@ public SettingManagementStore( GuidGenerator = guidGenerator; Cache = cache; SettingDefinitionManager = settingDefinitionManager; + Logger = NullLogger.Instance; } [UnitOfWork] @@ -37,7 +42,7 @@ public virtual async Task GetOrNullAsync(string name, string providerNam [UnitOfWork] public virtual async Task SetAsync(string name, string value, string providerName, string providerKey) { - var setting = await SettingRepository.FindAsync(name, providerName, providerKey); + var setting = await FindAndDeleteDuplicatesAsync(name, providerName, providerKey); if (setting == null) { setting = new Setting(GuidGenerator.Create(), name, value, providerName, providerKey); @@ -55,16 +60,22 @@ public virtual async Task SetAsync(string name, string value, string providerNam public virtual async Task> GetListAsync(string providerName, string providerKey) { var settings = await SettingRepository.GetListAsync(providerName, providerKey); - return settings.Select(s => new SettingValue(s.Name, s.Value)).ToList(); + return (await CreateSettingValueDictionaryAsync(settings, providerName, providerKey)) + .Select(s => new SettingValue(s.Key, s.Value)) + .ToList(); } [UnitOfWork] public virtual async Task DeleteAsync(string name, string providerName, string providerKey) { - var setting = await SettingRepository.FindAsync(name, providerName, providerKey); - if (setting != null) + var settings = await SettingRepository.GetListAsync(new[] { name }, providerName, providerKey); + if (settings.Any()) { - await SettingRepository.DeleteAsync(setting, true); + foreach (var setting in settings) + { + await SettingRepository.DeleteAsync(setting, true); + } + await Cache.RemoveAsync(CalculateCacheKey(name, providerName, providerKey), considerUow: true); } } @@ -93,8 +104,10 @@ private async Task SetCacheItemsAsync( SettingCacheItem currentCacheItem) { var settingDefinitions = await SettingDefinitionManager.GetAllAsync(); - var settingsDictionary = (await SettingRepository.GetListAsync(providerName, providerKey)) - .ToDictionary(s => s.Name, s => s.Value); + var settingsDictionary = await CreateSettingValueDictionaryAsync( + await SettingRepository.GetListAsync(providerName, providerKey), + providerName, + providerKey); var cacheItems = new List>(); @@ -179,8 +192,10 @@ private async Task>> SetCacheItemsAs var settingNames = new HashSet(notCacheKeys.Select(GetSettingNameFormCacheKeyOrNull)); var settingDefinitions = (await SettingDefinitionManager.GetAllAsync()).Where(x => settingNames.Contains(x.Name)); - var settingsDictionary = (await SettingRepository.GetListAsync(settingNames.ToArray(), providerName, providerKey)) - .ToDictionary(s => s.Name, s => s.Value); + var settingsDictionary = await CreateSettingValueDictionaryAsync( + await SettingRepository.GetListAsync(settingNames.ToArray(), providerName, providerKey), + providerName, + providerKey); var cacheItems = new List>(); @@ -201,6 +216,55 @@ private async Task>> SetCacheItemsAs } + protected virtual async Task FindAndDeleteDuplicatesAsync(string name, string providerName, string providerKey) + { + var settings = await SettingRepository.GetListAsync(new[] { name }, providerName, providerKey); + if (settings.Count <= 1) + { + return settings.FirstOrDefault(); + } + + var setting = await SettingRepository.FindAsync(name, providerName, providerKey); + if (setting == null) + { + return null; + } + + foreach (var duplicate in settings.Where(x => x.Id != setting.Id)) + { + await SettingRepository.DeleteAsync(duplicate, true); + } + + return setting; + } + + protected virtual async Task> CreateSettingValueDictionaryAsync( + List settings, + string providerName, + string providerKey) + { + var settingsDictionary = new Dictionary(); + + foreach (var settingGroup in settings.GroupBy(s => s.Name)) + { + if (settingGroup.Count() == 1) + { + settingsDictionary[settingGroup.Key] = settingGroup.First().Value; + continue; + } + + // FindAsync returns the same record that SetAsync updates, so reads and writes agree until the duplicates are deleted. + Logger.LogWarning( + "Found {Count} setting records for Name = {Name}, ProviderName = {ProviderName}, ProviderKey = {ProviderKey}. The duplicates will be deleted the next time the setting is saved.", + settingGroup.Count(), settingGroup.Key, providerName, providerKey); + + var setting = await SettingRepository.FindAsync(settingGroup.Key, providerName, providerKey); + settingsDictionary[settingGroup.Key] = (setting ?? settingGroup.First()).Value; + } + + return settingsDictionary; + } + protected virtual string CalculateCacheKey(string name, string providerName, string providerKey) { return SettingCacheItem.CalculateCacheKey(name, providerName, providerKey); diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/DuplicateSettingRecords_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/DuplicateSettingRecords_Tests.cs new file mode 100644 index 00000000000..b0174beaca6 --- /dev/null +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.EntityFrameworkCore.Tests/Volo/Abp/SettingManagement/EntityFrameworkCore/DuplicateSettingRecords_Tests.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.SettingManagement.EntityFrameworkCore; + +public class DuplicateSettingRecords_Tests : DuplicateSettingRecords_Tests +{ + +} diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.MongoDB.Tests/Volo/Abp/SettingManagement/MongoDB/DuplicateSettingRecords_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.MongoDB.Tests/Volo/Abp/SettingManagement/MongoDB/DuplicateSettingRecords_Tests.cs new file mode 100644 index 00000000000..35ddd7afb0b --- /dev/null +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.MongoDB.Tests/Volo/Abp/SettingManagement/MongoDB/DuplicateSettingRecords_Tests.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace Volo.Abp.SettingManagement.MongoDB; + +[Collection(MongoTestCollection.Name)] +public class DuplicateSettingRecords_Tests : DuplicateSettingRecords_Tests +{ + +} diff --git a/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/DuplicateSettingRecords_Tests.cs b/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/DuplicateSettingRecords_Tests.cs new file mode 100644 index 00000000000..b9b1f299dee --- /dev/null +++ b/modules/setting-management/test/Volo.Abp.SettingManagement.TestBase/Volo/Abp/SettingManagement/DuplicateSettingRecords_Tests.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.Settings; +using Xunit; + +namespace Volo.Abp.SettingManagement; + +public abstract class DuplicateSettingRecords_Tests : SettingManagementTestBase + where TStartupModule : IAbpModule +{ + protected ISettingManagementStore SettingManagementStore { get; } + protected ISettingRepository SettingRepository { get; } + + protected DuplicateSettingRecords_Tests() + { + SettingManagementStore = GetRequiredService(); + SettingRepository = GetRequiredService(); + } + + [Fact] + public async Task GetOrNullAsync_Should_Not_Fail_When_Duplicate_Global_Settings_Exist() + { + await InsertDuplicateGlobalSettingAsync("MySetting1", "43"); + + var value = await SettingManagementStore.GetOrNullAsync("MySetting2", GlobalSettingValueProvider.ProviderName, null); + value.ShouldBe("default-store-value"); + + var duplicatedValue = await SettingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + duplicatedValue.ShouldBe((await SettingRepository.FindAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null)).Value); + } + + [Fact] + public async Task GetListAsync_Should_Not_Fail_When_Duplicate_Global_Settings_Exist() + { + await InsertDuplicateGlobalSettingAsync("MySetting1", "43"); + + var result = await SettingManagementStore.GetListAsync( + new[] { "MySetting1", "MySetting2" }, + GlobalSettingValueProvider.ProviderName, + null); + + result.First(x => x.Name == "MySetting1").Value.ShouldBe((await SettingRepository.FindAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null)).Value); + result.First(x => x.Name == "MySetting2").Value.ShouldBe("default-store-value"); + } + + [Fact] + public async Task GetListAsync_By_Provider_Should_Return_One_Value_Per_Setting_When_Duplicate_Global_Settings_Exist() + { + await InsertDuplicateGlobalSettingAsync("MySetting1", "43"); + + var result = await SettingManagementStore.GetListAsync(GlobalSettingValueProvider.ProviderName, null); + + result.Count(x => x.Name == "MySetting1").ShouldBe(1); + result.Single(x => x.Name == "MySetting1").Value.ShouldBe((await SettingRepository.FindAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null)).Value); + } + + [Fact] + public async Task SetAsync_Should_Remove_Duplicate_Global_Settings() + { + await InsertDuplicateGlobalSettingAsync("MySetting1", "43"); + + await SettingManagementStore.SetAsync("MySetting1", "44", GlobalSettingValueProvider.ProviderName, null); + + var settings = await SettingRepository.GetListAsync(new[] { "MySetting1" }, GlobalSettingValueProvider.ProviderName, null); + settings.Count.ShouldBe(1); + settings[0].Value.ShouldBe("44"); + (await SettingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null)).ShouldBe("44"); + } + + [Fact] + public async Task DeleteAsync_Should_Remove_Duplicate_Global_Settings() + { + await InsertDuplicateGlobalSettingAsync("MySetting1", "43"); + + await SettingManagementStore.DeleteAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null); + + (await SettingRepository.GetListAsync(new[] { "MySetting1" }, GlobalSettingValueProvider.ProviderName, null)).ShouldBeEmpty(); + (await SettingManagementStore.GetOrNullAsync("MySetting1", GlobalSettingValueProvider.ProviderName, null)).ShouldBeNull(); + } + + private async Task InsertDuplicateGlobalSettingAsync(string name, string value) + { + await SettingRepository.InsertAsync(new Setting(Guid.NewGuid(), name, value, GlobalSettingValueProvider.ProviderName)); + (await SettingRepository.GetListAsync(new[] { name }, GlobalSettingValueProvider.ProviderName, null)).Count.ShouldBe(2); + } +} From 2deab538a5db2a5c8b9069712e50f9d6d660b952 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 24 Sep 2026 11:11:25 +0800 Subject: [PATCH 2/3] Keep FindAsync as the primary lookup when removing duplicates --- .../Abp/FeatureManagement/FeatureManagementStore.cs | 10 +++------- .../Abp/SettingManagement/SettingManagementStore.cs | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs index 34ca65eb6a4..e03aa04932f 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementStore.cs @@ -122,19 +122,15 @@ await FeatureValueRepository.GetListAsync(providerName, providerKey), protected virtual async Task FindAndDeleteDuplicatesAsync(string name, string providerName, string providerKey) { - var featureValues = await FeatureValueRepository.FindAllAsync(name, providerName, providerKey); - if (featureValues.Count <= 1) - { - return featureValues.FirstOrDefault(); - } - var featureValue = await FeatureValueRepository.FindAsync(name, providerName, providerKey); if (featureValue == null) { return null; } - foreach (var duplicate in featureValues.Where(x => x.Id != featureValue.Id)) + var duplicates = (await FeatureValueRepository.FindAllAsync(name, providerName, providerKey)) + .Where(x => x.Id != featureValue.Id); + foreach (var duplicate in duplicates) { await FeatureValueRepository.DeleteAsync(duplicate, true); } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs index 3a0ff8100a0..20626026af5 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs @@ -218,19 +218,15 @@ await SettingRepository.GetListAsync(settingNames.ToArray(), providerName, provi protected virtual async Task FindAndDeleteDuplicatesAsync(string name, string providerName, string providerKey) { - var settings = await SettingRepository.GetListAsync(new[] { name }, providerName, providerKey); - if (settings.Count <= 1) - { - return settings.FirstOrDefault(); - } - var setting = await SettingRepository.FindAsync(name, providerName, providerKey); if (setting == null) { return null; } - foreach (var duplicate in settings.Where(x => x.Id != setting.Id)) + var duplicates = (await SettingRepository.GetListAsync(new[] { name }, providerName, providerKey)) + .Where(x => x.Id != setting.Id); + foreach (var duplicate in duplicates) { await SettingRepository.DeleteAsync(duplicate, true); } From 769ccc472e393affc43d15d9efaef2fc0121c281 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 24 Sep 2026 11:22:29 +0800 Subject: [PATCH 3/3] Delete all duplicate grants in ResourcePermissionManager.DeleteAsync --- .../ResourcePermissionManager.cs | 4 +-- .../ResourcePermissionManager_Tests.cs | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs index 5e8c8a0454f..28c3a56386e 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/ResourcePermissionManager.cs @@ -375,8 +375,8 @@ public virtual async Task DeleteAsync(string name, string resourceName, string r throw new AbpException($"The resource permission management provider '{providerName}' is not available in the current context."); } - var permissionGrant = await ResourcePermissionGrantRepository.FindAsync(name, resourceName, resourceKey, providerName, providerKey); - if (permissionGrant != null) + var permissionGrants = await ResourcePermissionGrantRepository.GetListAsync(new[] { name }, resourceName, resourceKey, providerName, providerKey); + foreach (var permissionGrant in permissionGrants) { await ResourcePermissionGrantRepository.DeleteAsync(permissionGrant, true); } diff --git a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManager_Tests.cs b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManager_Tests.cs index dc5cb4f4bdb..4a3e69fe99c 100644 --- a/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManager_Tests.cs +++ b/modules/permission-management/test/Volo.Abp.PermissionManagement.Domain.Tests/Volo/Abp/PermissionManagement/ResourcePermissionManager_Tests.cs @@ -341,6 +341,41 @@ await _resourcePermissionManager.DeleteAsync( "Test")).ShouldBeNull(); } + [Fact] + public async Task DeleteByNameAsync_Should_Delete_Duplicate_Host_Grants() + { + for (var i = 0; i < 2; i++) + { + await _resourcePermissionGrantRepository.InsertAsync(new ResourcePermissionGrant( + Guid.NewGuid(), + "MyResourcePermission1", + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test") + ); + } + (await _resourcePermissionGrantRepository.GetListAsync(new[] { "MyResourcePermission1" }, + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test")).Count.ShouldBe(2); + + await _resourcePermissionManager.DeleteAsync( + "MyResourcePermission1", + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test"); + + (await _resourcePermissionGrantRepository.FindAsync( + "MyResourcePermission1", + TestEntityResource.ResourceName, + TestEntityResource.ResourceKey1, + "Test", + "Test")).ShouldBeNull(); + } + [Fact] public async Task GetProviderKeyLookupServicesAsync_Should_Not_Return_Unavailable_Services() {