From d62540a492d2adc3d52bc31b080dcf57e3bd2e7d Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 1 Sep 2022 18:06:17 +0800 Subject: [PATCH] Implement infrastructure, Domain and EF Core modules. --- .../AbpApplicationConfigurationAppService.cs | 2 +- .../Volo/Abp/Features/AbpFeatureOptions.cs | 10 +- .../Volo/Abp/Features/FeatureChecker.cs | 2 +- .../Volo/Abp/Features/FeatureDefinition.cs | 13 +- .../Abp/Features/FeatureDefinitionManager.cs | 120 +++---- .../Abp/Features/FeatureGroupDefinition.cs | 12 +- .../Abp/Features/ICanCreateChildFeature.cs | 16 + .../IDynamicFeatureDefinitionStore.cs | 13 + .../Abp/Features/IFeatureDefinitionManager.cs | 9 +- .../Features/IStaticFeatureDefinitionStore.cs | 13 + .../NullDynamicFeatureDefinitionStore.cs | 33 ++ .../Features/StaticFeatureDefinitionStore.cs | 121 +++++++ .../FeatureDefinitionManager_Tests.cs | 21 +- ...ureManagementApplicationContractsModule.cs | 4 +- .../FeatureManagement/FeatureAppService.cs | 2 +- ...Abp.FeatureManagement.Domain.Shared.csproj | 1 + .../FeatureDefinitionRecordConsts.cs | 16 + .../FeatureGroupDefinitionRecordConsts.cs | 8 + .../FeatureManagement/FeatureValueConsts.cs | 0 .../JsonConverters/IValueValidatorFactory.cs | 0 .../NewtonsoftStringValueTypeJsonConverter.cs | 4 +- ...ctionStringValueItemSourceJsonConverter.cs | 0 .../StringValueTypeJsonConverter.cs | 4 +- .../JsonConverters/ValueValidatorFactory.cs | 0 .../ValueValidatorJsonConverter.cs | 4 +- .../ValueValidatorFactoryOptions.cs} | 8 +- .../Volo.Abp.FeatureManagement.Domain.csproj | 4 + .../DynamicFeatureDefinitionStore.cs | 171 ++++++++++ ...amicFeatureDefinitionStoreInMemoryCache.cs | 119 +++++++ .../FeatureDefinitionRecord.cs | 205 ++++++++++++ .../FeatureDefinitionSerializer.cs | 102 ++++++ .../FeatureGroupDefinitionRecord.cs | 85 +++++ .../FeatureManagementOptions.cs | 10 + .../FeatureManagementStore.cs | 2 +- .../Abp/FeatureManagement/FeatureManager.cs | 6 +- ...amicFeatureDefinitionStoreInMemoryCache.cs | 26 ++ .../IFeatureDefinitionRecordRepository.cs | 9 + .../IFeatureDefinitionSerializer.cs | 15 + ...IFeatureGroupDefinitionRecordRepository.cs | 9 + .../FeatureManagement/IStaticFeatureSaver.cs | 8 + .../FeatureManagement/StaticFeatureSaver.cs | 294 ++++++++++++++++++ .../StringValueTypeSerializer.cs | 25 ++ ...tureManagementEntityFrameworkCoreModule.cs | 6 +- ...EfCoreFeatureDefinitionRecordRepository.cs | 16 + ...eFeatureGroupDefinitionRecordRepository.cs | 16 + .../FeatureManagementDbContext.cs | 4 + ...agementDbContextModelCreatingExtensions.cs | 36 ++- .../IFeatureManagementDbContext.cs | 4 + .../AbpFeatureManagementHttpApiModule.cs | 2 +- .../StringValueJsonConverter_Tests.cs | 2 +- 50 files changed, 1487 insertions(+), 125 deletions(-) create mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/ICanCreateChildFeature.cs create mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/IDynamicFeatureDefinitionStore.cs create mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/IStaticFeatureDefinitionStore.cs create mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/NullDynamicFeatureDefinitionStore.cs create mode 100644 framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureDefinitionRecordConsts.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecordConsts.cs rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Domain => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/FeatureValueConsts.cs (100%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/JsonConverters/IValueValidatorFactory.cs (100%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs (94%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs (100%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs (92%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorFactory.cs (100%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts => Volo.Abp.FeatureManagement.Domain.Shared}/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs (93%) rename modules/feature-management/src/{Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsOptions.cs => Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/ValueValidatorFactoryOptions.cs} (82%) create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStore.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStoreInMemoryCache.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionRecord.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionSerializer.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecord.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IDynamicFeatureDefinitionStoreInMemoryCache.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionRecordRepository.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionSerializer.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureGroupDefinitionRecordRepository.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IStaticFeatureSaver.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureSaver.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StringValueTypeSerializer.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureDefinitionRecordRepository.cs create mode 100644 modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureGroupDefinitionRecordRepository.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 7bcc0b3cd9..a22cefc9e1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -291,7 +291,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp { var result = new ApplicationFeatureConfigurationDto(); - foreach (var featureDefinition in _featureDefinitionManager.GetAll()) + foreach (var featureDefinition in await _featureDefinitionManager.GetAllAsync()) { if (!featureDefinition.IsVisibleToClients) { diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeatureOptions.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeatureOptions.cs index 22c1067082..87ff7716db 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeatureOptions.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/AbpFeatureOptions.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Collections; +using System.Collections.Generic; +using Volo.Abp.Collections; namespace Volo.Abp.Features; @@ -8,9 +9,16 @@ public class AbpFeatureOptions public ITypeList ValueProviders { get; } + public HashSet DeletedFeatures { get; } + + public HashSet DeletedFeatureGroups { get; } + public AbpFeatureOptions() { DefinitionProviders = new TypeList(); ValueProviders = new TypeList(); + + DeletedFeatures = new HashSet(); + DeletedFeatureGroups = new HashSet(); } } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs index 580881ce84..9554860078 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs @@ -37,7 +37,7 @@ public class FeatureChecker : FeatureCheckerBase public override async Task GetOrNullAsync(string name) { - var featureDefinition = FeatureDefinitionManager.Get(name); + var featureDefinition = await FeatureDefinitionManager.GetAsync(name); var providers = Enumerable .Reverse(Providers); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs index 3123faefc2..5ea7377fd7 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs @@ -7,7 +7,7 @@ using Volo.Abp.Validation.StringValues; namespace Volo.Abp.Features; -public class FeatureDefinition +public class FeatureDefinition : ICanCreateChildFeature { /// /// Unique name of the feature. @@ -178,6 +178,17 @@ public class FeatureDefinition _children.Remove(featureToRemove); } + public FeatureDefinition CreateChildFeature(string name, + string defaultValue = null, + ILocalizableString displayName = null, + ILocalizableString description = null, + IStringValueType valueType = null, + bool isVisibleToClients = true, + bool isAvailableToHost = true) + { + return this.CreateChild(name, defaultValue, displayName, description, valueType, isVisibleToClients, isAvailableToHost); + } + public override string ToString() { return $"[{nameof(FeatureDefinition)}: {Name}]"; diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs index c39db18771..d0293d5859 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs @@ -2,120 +2,70 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using System.Threading.Tasks; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Features; public class FeatureDefinitionManager : IFeatureDefinitionManager, ISingletonDependency { - protected IDictionary FeatureGroupDefinitions => _lazyFeatureGroupDefinitions.Value; - private readonly Lazy> _lazyFeatureGroupDefinitions; - - protected IDictionary FeatureDefinitions => _lazyFeatureDefinitions.Value; - private readonly Lazy> _lazyFeatureDefinitions; - - protected AbpFeatureOptions Options { get; } - - private readonly IServiceScopeFactory _serviceScopeFactory; + protected IStaticFeatureDefinitionStore StaticStore; + protected IDynamicFeatureDefinitionStore DynamicStore; public FeatureDefinitionManager( - IOptions options, - IServiceScopeFactory serviceScopeFactory) + IStaticFeatureDefinitionStore staticStore, + IDynamicFeatureDefinitionStore dynamicStore) { - _serviceScopeFactory = serviceScopeFactory; - Options = options.Value; - - _lazyFeatureDefinitions = new Lazy>( - CreateFeatureDefinitions, - isThreadSafe: true - ); - - _lazyFeatureGroupDefinitions = new Lazy>( - CreateFeatureGroupDefinitions, - isThreadSafe: true - ); + StaticStore = staticStore; + DynamicStore = dynamicStore; } - public virtual FeatureDefinition Get(string name) + public virtual async Task GetAsync(string name) { - Check.NotNull(name, nameof(name)); - - var feature = GetOrNull(name); - - if (feature == null) + var permission = await GetOrNullAsync(name); + if (permission == null) { throw new AbpException("Undefined feature: " + name); } - return feature; - } - - public virtual IReadOnlyList GetAll() - { - return FeatureDefinitions.Values.ToImmutableList(); - } - - public virtual FeatureDefinition GetOrNull(string name) - { - return FeatureDefinitions.GetOrDefault(name); - } - - public IReadOnlyList GetGroups() - { - return FeatureGroupDefinitions.Values.ToImmutableList(); + return permission; } - protected virtual Dictionary CreateFeatureDefinitions() + public virtual async Task GetOrNullAsync(string name) { - var features = new Dictionary(); - - foreach (var groupDefinition in FeatureGroupDefinitions.Values) - { - foreach (var feature in groupDefinition.Features) - { - AddFeatureToDictionaryRecursively(features, feature); - } - } + Check.NotNull(name, nameof(name)); - return features; + return await StaticStore.GetOrNullAsync(name) ?? + await DynamicStore.GetOrNullAsync(name); } - protected virtual void AddFeatureToDictionaryRecursively( - Dictionary features, - FeatureDefinition feature) + public virtual async Task> GetAllAsync() { - if (features.ContainsKey(feature.Name)) - { - throw new AbpException("Duplicate feature name: " + feature.Name); - } + var staticFeatures = await StaticStore.GetFeaturesAsync(); + var staticFeatureNames = staticFeatures + .Select(p => p.Name) + .ToImmutableHashSet(); - features[feature.Name] = feature; + var dynamicFeatures = await DynamicStore.GetFeaturesAsync(); - foreach (var child in feature.Children) - { - AddFeatureToDictionaryRecursively(features, child); - } + /* We prefer static features over dynamics */ + return staticFeatures.Concat( + dynamicFeatures.Where(d => !staticFeatureNames.Contains(d.Name)) + ).ToImmutableList(); } - protected virtual Dictionary CreateFeatureGroupDefinitions() + public virtual async Task> GetGroupsAsync() { - var context = new FeatureDefinitionContext(); + var staticGroups = await StaticStore.GetGroupsAsync(); + var staticGroupNames = staticGroups + .Select(p => p.Name) + .ToImmutableHashSet(); - using (var scope = _serviceScopeFactory.CreateScope()) - { - var providers = Options - .DefinitionProviders - .Select(p => scope.ServiceProvider.GetRequiredService(p) as IFeatureDefinitionProvider) - .ToList(); - - foreach (var provider in providers) - { - provider.Define(context); - } - } + var dynamicGroups = await DynamicStore.GetGroupsAsync(); - return context.Groups; + /* We prefer static groups over dynamics */ + return staticGroups.Concat( + dynamicGroups.Where(d => !staticGroupNames.Contains(d.Name)) + ).ToImmutableList(); } } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureGroupDefinition.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureGroupDefinition.cs index cba2e06584..e584a95c50 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureGroupDefinition.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureGroupDefinition.cs @@ -5,7 +5,7 @@ using Volo.Abp.Validation.StringValues; namespace Volo.Abp.Features; -public class FeatureGroupDefinition +public class FeatureGroupDefinition : ICanCreateChildFeature { /// /// Unique name of the group. @@ -69,6 +69,16 @@ public class FeatureGroupDefinition return feature; } + public FeatureDefinition CreateChildFeature(string name, + string defaultValue = null, + ILocalizableString displayName = null, + ILocalizableString description = null, + IStringValueType valueType = null, + bool isVisibleToClients = true, + bool isAvailableToHost = true) + { + return AddFeature(name, defaultValue, displayName, description, valueType, isVisibleToClients); + } public virtual List GetFeaturesWithChildren() { var features = new List(); diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/ICanCreateChildFeature.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/ICanCreateChildFeature.cs new file mode 100644 index 0000000000..ecba11e31e --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/ICanCreateChildFeature.cs @@ -0,0 +1,16 @@ +using Volo.Abp.Localization; +using Volo.Abp.Validation.StringValues; + +namespace Volo.Abp.Features; + +public interface ICanCreateChildFeature +{ + FeatureDefinition CreateChildFeature( + string name, + string defaultValue = null, + ILocalizableString displayName = null, + ILocalizableString description = null, + IStringValueType valueType = null, + bool isVisibleToClients = true, + bool isAvailableToHost = true); +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IDynamicFeatureDefinitionStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IDynamicFeatureDefinitionStore.cs new file mode 100644 index 0000000000..90fe14a5e0 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IDynamicFeatureDefinitionStore.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Features; + +public interface IDynamicFeatureDefinitionStore +{ + Task GetOrNullAsync(string name); + + Task> GetFeaturesAsync(); + + Task> GetGroupsAsync(); +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs index ebef29b64a..f0201937df 100644 --- a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using JetBrains.Annotations; namespace Volo.Abp.Features; @@ -6,11 +7,11 @@ namespace Volo.Abp.Features; public interface IFeatureDefinitionManager { [NotNull] - FeatureDefinition Get([NotNull] string name); + Task GetAsync([NotNull] string name); - IReadOnlyList GetAll(); + Task> GetAllAsync(); - FeatureDefinition GetOrNull(string name); + Task GetOrNullAsync(string name); - IReadOnlyList GetGroups(); + Task> GetGroupsAsync(); } diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IStaticFeatureDefinitionStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IStaticFeatureDefinitionStore.cs new file mode 100644 index 0000000000..586747904e --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IStaticFeatureDefinitionStore.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Features; + +public interface IStaticFeatureDefinitionStore +{ + Task GetOrNullAsync(string name); + + Task> GetFeaturesAsync(); + + Task> GetGroupsAsync(); +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/NullDynamicFeatureDefinitionStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/NullDynamicFeatureDefinitionStore.cs new file mode 100644 index 0000000000..63e7b476c4 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/NullDynamicFeatureDefinitionStore.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features; + +public class NullDynamicFeatureDefinitionStore : IDynamicFeatureDefinitionStore, ISingletonDependency +{ + private static readonly Task CachedFeatureResult = Task.FromResult((FeatureDefinition)null); + + private static readonly Task> CachedFeaturesResult = + Task.FromResult((IReadOnlyList)Array.Empty().ToImmutableList()); + + private static readonly Task> CachedGroupsResult = + Task.FromResult((IReadOnlyList)Array.Empty().ToImmutableList()); + + public Task GetOrNullAsync(string name) + { + return CachedFeatureResult; + } + + public Task> GetFeaturesAsync() + { + return CachedFeaturesResult; + } + + public Task> GetGroupsAsync() + { + return CachedGroupsResult; + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs new file mode 100644 index 0000000000..7d683d6caf --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/StaticFeatureDefinitionStore.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features; + +public class StaticFeatureDefinitionStore: IStaticFeatureDefinitionStore, ISingletonDependency +{ + protected IDictionary FeatureGroupDefinitions => _lazyFeatureGroupDefinitions.Value; + private readonly Lazy> _lazyFeatureGroupDefinitions; + + protected IDictionary FeatureDefinitions => _lazyFeatureDefinitions.Value; + private readonly Lazy> _lazyFeatureDefinitions; + + protected AbpFeatureOptions Options { get; } + + private readonly IServiceProvider _serviceProvider; + + public StaticFeatureDefinitionStore( + IOptions options, + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + Options = options.Value; + + _lazyFeatureDefinitions = new Lazy>( + CreateFeatureDefinitions, + isThreadSafe: true + ); + + _lazyFeatureGroupDefinitions = new Lazy>( + CreateFeatureGroupDefinitions, + isThreadSafe: true + ); + } + + public virtual async Task GetAsync(string name) + { + Check.NotNull(name, nameof(name)); + + var feature = await GetOrNullAsync(name); + + if (feature == null) + { + throw new AbpException("Undefined feature: " + name); + } + + return feature; + } + + protected virtual Dictionary CreateFeatureDefinitions() + { + var features = new Dictionary(); + + foreach (var groupDefinition in FeatureGroupDefinitions.Values) + { + foreach (var feature in groupDefinition.Features) + { + AddFeatureToDictionaryRecursively(features, feature); + } + } + + return features; + } + + protected virtual void AddFeatureToDictionaryRecursively( + Dictionary features, + FeatureDefinition feature) + { + if (features.ContainsKey(feature.Name)) + { + throw new AbpException("Duplicate feature name: " + feature.Name); + } + + features[feature.Name] = feature; + + foreach (var child in feature.Children) + { + AddFeatureToDictionaryRecursively(features, child); + } + } + + protected virtual Dictionary CreateFeatureGroupDefinitions() + { + var context = new FeatureDefinitionContext(); + + using (var scope = _serviceProvider.CreateScope()) + { + var providers = Options + .DefinitionProviders + .Select(p => scope.ServiceProvider.GetRequiredService(p) as IFeatureDefinitionProvider) + .ToList(); + + foreach (var provider in providers) + { + provider.Define(context); + } + } + + return context.Groups; + } + + public virtual Task GetOrNullAsync(string name) + { + return Task.FromResult(FeatureDefinitions.GetOrDefault(name)); + } + + public virtual Task> GetFeaturesAsync() + { + return Task.FromResult>(FeatureDefinitions.Values.ToList()); + } + + public virtual Task> GetGroupsAsync() + { + return Task.FromResult>(FeatureGroupDefinitions.Values.ToList()); + } +} diff --git a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/FeatureDefinitionManager_Tests.cs b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/FeatureDefinitionManager_Tests.cs index 246ffe537d..5e3cb96741 100644 --- a/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/FeatureDefinitionManager_Tests.cs +++ b/framework/test/Volo.Abp.Features.Tests/Volo/Abp/Features/FeatureDefinitionManager_Tests.cs @@ -1,4 +1,5 @@ -using Shouldly; +using System.Threading.Tasks; +using Shouldly; using Xunit; namespace Volo.Abp.Features; @@ -13,22 +14,22 @@ public class FeatureDefinitionManager_Tests : FeatureTestBase } [Fact] - public void Should_Get_Defined_Features() + public async Task Should_Get_Defined_Features() { - _featureDefinitionManager.GetOrNull("BooleanTestFeature1").ShouldNotBeNull(); - _featureDefinitionManager.Get("BooleanTestFeature1").Name.ShouldBe("BooleanTestFeature1"); + await _featureDefinitionManager.GetOrNullAsync("BooleanTestFeature1").ShouldNotBeNull(); + (await _featureDefinitionManager.GetAsync("BooleanTestFeature1")).Name.ShouldBe("BooleanTestFeature1"); - _featureDefinitionManager.GetOrNull("IntegerTestFeature1").ShouldNotBeNull(); - _featureDefinitionManager.Get("IntegerTestFeature1").Name.ShouldBe("IntegerTestFeature1"); + await _featureDefinitionManager.GetOrNullAsync("IntegerTestFeature1").ShouldNotBeNull(); + (await _featureDefinitionManager.GetAsync("IntegerTestFeature1")).Name.ShouldBe("IntegerTestFeature1"); } [Fact] - public void Should_Not_Get_Undefined_Features() + public async Task Should_Not_Get_Undefined_Features() { - _featureDefinitionManager.GetOrNull("UndefinedFeature").ShouldBeNull(); - Assert.Throws(() => + (await _featureDefinitionManager.GetOrNullAsync("UndefinedFeature")).ShouldBeNull(); + await Assert.ThrowsAsync(async () => { - _featureDefinitionManager.Get("UndefinedFeature"); + await _featureDefinitionManager.GetAsync("UndefinedFeature"); }); } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsModule.cs index cfbcf1c95e..0e16a5607e 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsModule.cs @@ -26,8 +26,8 @@ public class AbpFeatureManagementApplicationContractsModule : AbpModule options.FileSets.AddEmbedded(); }); - var contractsOptionsActions = context.Services.GetPreConfigureActions(); - Configure(options => + var contractsOptionsActions = context.Services.GetPreConfigureActions(); + Configure(options => { contractsOptionsActions.Configure(options); }); diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs index 095840655c..0e14c64897 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Application/Volo/Abp/FeatureManagement/FeatureAppService.cs @@ -35,7 +35,7 @@ public class FeatureAppService : FeatureManagementAppServiceBase, IFeatureAppSer Groups = new List() }; - foreach (var group in FeatureDefinitionManager.GetGroups()) + foreach (var group in await FeatureDefinitionManager.GetGroupsAsync()) { var groupDto = new FeatureGroupDto { diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo.Abp.FeatureManagement.Domain.Shared.csproj b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo.Abp.FeatureManagement.Domain.Shared.csproj index 20e31fe161..9f1260ee56 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo.Abp.FeatureManagement.Domain.Shared.csproj +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo.Abp.FeatureManagement.Domain.Shared.csproj @@ -16,6 +16,7 @@ + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureDefinitionRecordConsts.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureDefinitionRecordConsts.cs new file mode 100644 index 0000000000..4088424774 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureDefinitionRecordConsts.cs @@ -0,0 +1,16 @@ +namespace Volo.Abp.FeatureManagement; + +public static class FeatureDefinitionRecordConsts +{ + public static int MaxNameLength { get; set; } = 128; + + public static int MaxDisplayNameLength { get; set; } = 256; + + public static int MaxDescriptionLength { get; set; } = 256; + + public static int MaxDefaultValueLength { get; set; } = 256; + + public static int MaxAllowedProvidersLength { get; set; } = 256; + + public static int MaxValueTypeLength { get; set; } = 256; +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecordConsts.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecordConsts.cs new file mode 100644 index 0000000000..e90fdd9479 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecordConsts.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.FeatureManagement; + +public static class FeatureGroupDefinitionRecordConsts +{ + public static int MaxNameLength { get; set; } = 128; + + public static int MaxDisplayNameLength { get; set; } = 256; +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureValueConsts.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureValueConsts.cs similarity index 100% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureValueConsts.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/FeatureValueConsts.cs diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/IValueValidatorFactory.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/IValueValidatorFactory.cs similarity index 100% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/IValueValidatorFactory.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/IValueValidatorFactory.cs diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs similarity index 94% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs index aa9a913718..2bb3e1fd2d 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs @@ -14,9 +14,9 @@ public class NewtonsoftStringValueTypeJsonConverter : JsonConverter, ITransientD { public override bool CanWrite => false; - protected readonly AbpFeatureManagementApplicationContractsOptions Options; + protected readonly ValueValidatorFactoryOptions Options; - public NewtonsoftStringValueTypeJsonConverter(IOptions options) + public NewtonsoftStringValueTypeJsonConverter(IOptions options) { Options = options.Value; } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs similarity index 100% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs similarity index 92% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs index ca91efd58a..fd463866d6 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs @@ -12,9 +12,9 @@ public class StringValueTypeJsonConverter : JsonConverter private JsonSerializerOptions _writeJsonSerializerOptions; - protected readonly AbpFeatureManagementApplicationContractsOptions Options; + protected readonly ValueValidatorFactoryOptions Options; - public StringValueTypeJsonConverter(AbpFeatureManagementApplicationContractsOptions options) + public StringValueTypeJsonConverter(ValueValidatorFactoryOptions options) { Options = options; } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorFactory.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorFactory.cs similarity index 100% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorFactory.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorFactory.cs diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs similarity index 93% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs index 69f954f4ee..b8a4387de9 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs @@ -14,9 +14,9 @@ public class ValueValidatorJsonConverter : JsonConverter private JsonSerializerOptions _writeJsonSerializerOptions; - protected readonly AbpFeatureManagementApplicationContractsOptions Options; + protected readonly ValueValidatorFactoryOptions Options; - public ValueValidatorJsonConverter(AbpFeatureManagementApplicationContractsOptions options) + public ValueValidatorJsonConverter(ValueValidatorFactoryOptions options) { Options = options; } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsOptions.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/ValueValidatorFactoryOptions.cs similarity index 82% rename from modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsOptions.cs rename to modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/ValueValidatorFactoryOptions.cs index 6c99bb648f..110e4752c6 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/AbpFeatureManagementApplicationContractsOptions.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/ValueValidatorFactoryOptions.cs @@ -4,13 +4,13 @@ using Volo.Abp.Validation.StringValues; namespace Volo.Abp.FeatureManagement; -public class AbpFeatureManagementApplicationContractsOptions +public class ValueValidatorFactoryOptions { public HashSet ValueValidatorFactory { get; } - - public AbpFeatureManagementApplicationContractsOptions() + + public ValueValidatorFactoryOptions() { - ValueValidatorFactory = new HashSet + ValueValidatorFactory = new HashSet { new ValueValidatorFactory("NULL"), new ValueValidatorFactory("BOOLEAN"), diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.csproj b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.csproj index 10033847c2..2e5185c28f 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.csproj +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo.Abp.FeatureManagement.Domain.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStore.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStore.cs new file mode 100644 index 0000000000..d8c3bca255 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStore.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Features; +using Volo.Abp.Threading; + +namespace Volo.Abp.FeatureManagement; + +[Dependency(ReplaceServices = true)] +public class DynamicFeatureDefinitionStore : IDynamicFeatureDefinitionStore, ITransientDependency +{ + protected IFeatureGroupDefinitionRecordRepository FeatureGroupRepository { get; } + protected IFeatureDefinitionRecordRepository FeatureRepository { get; } + protected IFeatureDefinitionSerializer FeatureDefinitionSerializer { get; } + protected IDynamicFeatureDefinitionStoreInMemoryCache StoreCache { get; } + protected IDistributedCache DistributedCache { get; } + protected IAbpDistributedLock DistributedLock { get; } + public FeatureManagementOptions FeatureManagementOptions { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + + public DynamicFeatureDefinitionStore( + IFeatureGroupDefinitionRecordRepository featureGroupRepository, + IFeatureDefinitionRecordRepository featureRepository, + IFeatureDefinitionSerializer featureDefinitionSerializer, + IDynamicFeatureDefinitionStoreInMemoryCache storeCache, + IDistributedCache distributedCache, + IOptions cacheOptions, + IOptions featureManagementOptions, + IAbpDistributedLock distributedLock) + { + FeatureGroupRepository = featureGroupRepository; + FeatureRepository = featureRepository; + FeatureDefinitionSerializer = featureDefinitionSerializer; + StoreCache = storeCache; + DistributedCache = distributedCache; + DistributedLock = distributedLock; + FeatureManagementOptions = featureManagementOptions.Value; + CacheOptions = cacheOptions.Value; + } + + public virtual async Task GetOrNullAsync(string name) + { + if (!FeatureManagementOptions.IsDynamicFeatureStoreEnabled) + { + return null; + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetFeatureOrNull(name); + } + } + + public virtual async Task> GetFeaturesAsync() + { + if (!FeatureManagementOptions.IsDynamicFeatureStoreEnabled) + { + return Array.Empty(); + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetFeatures().ToImmutableList(); + } + } + + public virtual async Task> GetGroupsAsync() + { + if (!FeatureManagementOptions.IsDynamicFeatureStoreEnabled) + { + return Array.Empty(); + } + + using (await StoreCache.SyncSemaphore.LockAsync()) + { + await EnsureCacheIsUptoDateAsync(); + return StoreCache.GetGroups().ToImmutableList(); + } + } + + protected virtual async Task EnsureCacheIsUptoDateAsync() + { + if (StoreCache.LastCheckTime.HasValue && + DateTime.Now.Subtract(StoreCache.LastCheckTime.Value).TotalSeconds < 30) + { + /* We get the latest feature with a small delay for optimization */ + return; + } + + var stampInDistributedCache = await GetOrSetStampInDistributedCache(); + + if (stampInDistributedCache == StoreCache.CacheStamp) + { + StoreCache.LastCheckTime = DateTime.Now; + return; + } + + await UpdateInMemoryStoreCache(); + + StoreCache.CacheStamp = stampInDistributedCache; + StoreCache.LastCheckTime = DateTime.Now; + } + + protected virtual async Task UpdateInMemoryStoreCache() + { + var featureGroupRecords = await FeatureGroupRepository.GetListAsync(); + var featureRecords = await FeatureRepository.GetListAsync(); + + await StoreCache.FillAsync(featureGroupRecords, featureRecords); + } + + protected virtual async Task GetOrSetStampInDistributedCache() + { + var cacheKey = GetCommonStampCacheKey(); + + var stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); + if (stampInDistributedCache != null) + { + return stampInDistributedCache; + } + + await using (var commonLockHandle = await DistributedLock + .TryAcquireAsync(GetCommonDistributedLockKey(), TimeSpan.FromMinutes(2))) + { + if (commonLockHandle == null) + { + /* This request will fail */ + throw new AbpException( + "Could not acquire distributed lock for feature definition common stamp check!" + ); + } + + stampInDistributedCache = await DistributedCache.GetStringAsync(cacheKey); + if (stampInDistributedCache != null) + { + return stampInDistributedCache; + } + + stampInDistributedCache = Guid.NewGuid().ToString(); + + await DistributedCache.SetStringAsync( + cacheKey, + stampInDistributedCache, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable? + } + ); + } + + return stampInDistributedCache; + } + + protected virtual string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryFeatureCacheStamp"; + } + + protected virtual string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpFeatureUpdateLock"; + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStoreInMemoryCache.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStoreInMemoryCache.cs new file mode 100644 index 0000000000..cefa0b3ade --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/DynamicFeatureDefinitionStoreInMemoryCache.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; +using Volo.Abp.Localization; + +namespace Volo.Abp.FeatureManagement; + +public class DynamicFeatureDefinitionStoreInMemoryCache: + IDynamicFeatureDefinitionStoreInMemoryCache, + ISingletonDependency +{ + public string CacheStamp { get; set; } + + protected IDictionary FeatureGroupDefinitions { get; } + protected IDictionary FeatureDefinitions { get; } + protected StringValueTypeSerializer StateCheckerSerializer { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + + public SemaphoreSlim SyncSemaphore { get; } = new(1, 1); + + public DateTime? LastCheckTime { get; set; } + + public DynamicFeatureDefinitionStoreInMemoryCache( + StringValueTypeSerializer stateCheckerSerializer, + ILocalizableStringSerializer localizableStringSerializer) + { + StateCheckerSerializer = stateCheckerSerializer; + LocalizableStringSerializer = localizableStringSerializer; + + FeatureGroupDefinitions = new Dictionary(); + FeatureDefinitions = new Dictionary(); + } + + public Task FillAsync( + List featureGroupRecords, + List featureRecords) + { + FeatureGroupDefinitions.Clear(); + FeatureDefinitions.Clear(); + + var context = new FeatureDefinitionContext(); + + foreach (var featureGroupRecord in featureGroupRecords) + { + var featureGroup = context.AddGroup( + featureGroupRecord.Name, + LocalizableStringSerializer.Deserialize(featureGroupRecord.DisplayName) + ); + + FeatureGroupDefinitions[featureGroup.Name] = featureGroup; + + foreach (var property in featureGroupRecord.ExtraProperties) + { + featureGroup[property.Key] = property.Value; + } + + var featureRecordsInThisGroup = featureRecords + .Where(p => p.GroupName == featureGroup.Name); + + foreach (var featureRecord in featureRecordsInThisGroup.Where(x => x.ParentName == null)) + { + AddFeatureRecursively(featureGroup, featureRecord, featureRecords); + } + } + + return Task.CompletedTask; + } + + public FeatureDefinition GetFeatureOrNull(string name) + { + return FeatureDefinitions.GetOrDefault(name); + } + + public IReadOnlyList GetFeatures() + { + return FeatureDefinitions.Values.ToList(); + } + + public IReadOnlyList GetGroups() + { + return FeatureGroupDefinitions.Values.ToList(); + } + + private void AddFeatureRecursively(ICanCreateChildFeature featureContainer, + FeatureDefinitionRecord featureRecord, + List allFeatureRecords) + { + var feature = featureContainer.CreateChildFeature( + featureRecord.Name, + featureRecord.DefaultValue, + LocalizableStringSerializer.Deserialize(featureRecord.DisplayName), + LocalizableStringSerializer.Deserialize(featureRecord.Description), + StateCheckerSerializer.Deserialize(featureRecord.ValueType), + featureRecord.IsVisibleToClients, + featureRecord.IsAvailableToHost + ); + + FeatureDefinitions[feature.Name] = feature; + + if (!featureRecord.AllowedProviders.IsNullOrWhiteSpace()) + { + feature.AllowedProviders.AddRange(featureRecord.AllowedProviders.Split(',')); + } + + foreach (var property in featureRecord.ExtraProperties) + { + feature[property.Key] = property.Value; + } + + foreach (var subFeature in allFeatureRecords.Where(p => p.ParentName == featureRecord.Name)) + { + AddFeatureRecursively(feature, subFeature, allFeatureRecords); + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionRecord.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionRecord.cs new file mode 100644 index 0000000000..618f52d7a1 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionRecord.cs @@ -0,0 +1,205 @@ +using System; +using System.Text.Json.Serialization; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.FeatureManagement; + +public class FeatureDefinitionRecord : BasicAggregateRoot, IHasExtraProperties +{ + /* Ignoring Id because it is different whenever we create an instance of + * this class, and we are using Json Serialize, than Hash to understand + * if feature definitions have changed (in StaticFeatureSaver.CalculateHash()). + */ + [JsonIgnore] //TODO: TODO: Use JSON modifier to ignore this property + public override Guid Id { get; protected set; } + + public string GroupName { get; set; } + + public string Name { get; set; } + + public string ParentName { get; set; } + + public string DisplayName { get; set; } + + public string Description { get; set; } + + public string DefaultValue { get; set; } + + public bool IsVisibleToClients { get; set; } + + public bool IsAvailableToHost { get; set; } + + /// + /// Comma separated list of provider names. + /// + public string AllowedProviders { get; set; } + + /// + /// Serialized string to store info about the ValueType. + /// + public string ValueType { get; set; } // ToggleStringValueType + + public ExtraPropertyDictionary ExtraProperties { get; protected set; } + + public FeatureDefinitionRecord() + { + IsVisibleToClients = true; + IsAvailableToHost = true; + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public FeatureDefinitionRecord( + Guid id, + string groupName, + string name, + string parentName, + string displayName = null, + string description = null, + string defaultValue = null, + bool isVisibleToClients = true, + bool isAvailableToHost = true, + string allowedProviders = null, + string valueType = null) + : base(id) + { + GroupName = Check.NotNullOrWhiteSpace(groupName, nameof(groupName), FeatureDefinitionRecordConsts.MaxNameLength); + Name = Check.NotNullOrWhiteSpace(name, nameof(name), FeatureDefinitionRecordConsts.MaxNameLength); + ParentName = Check.Length(parentName, nameof(parentName), FeatureDefinitionRecordConsts.MaxNameLength); + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), FeatureDefinitionRecordConsts.MaxDisplayNameLength); + + Description = Check.Length(description, nameof(description), FeatureDefinitionRecordConsts.MaxDescriptionLength); + DefaultValue = Check.NotNullOrWhiteSpace(defaultValue, nameof(defaultValue), FeatureDefinitionRecordConsts.MaxDefaultValueLength); + + IsVisibleToClients = isVisibleToClients; + IsAvailableToHost = isAvailableToHost; + + AllowedProviders = Check.Length(allowedProviders, nameof(allowedProviders), FeatureDefinitionRecordConsts.MaxAllowedProvidersLength); + ValueType = Check.NotNullOrWhiteSpace(valueType, nameof(valueType), FeatureDefinitionRecordConsts.MaxValueTypeLength); + + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + public bool HasSameData(FeatureDefinitionRecord otherRecord) + { + if (Name != otherRecord.Name) + { + return false; + } + + if (GroupName != otherRecord.GroupName) + { + return false; + } + + if (ParentName != otherRecord.ParentName) + { + return false; + } + + if (DisplayName != otherRecord.DisplayName) + { + return false; + } + + if (Description != otherRecord.Description) + { + return false; + } + + if (DefaultValue != otherRecord.DefaultValue) + { + return false; + } + + if (IsVisibleToClients != otherRecord.IsVisibleToClients) + { + return false; + } + + if (IsAvailableToHost != otherRecord.IsAvailableToHost) + { + return false; + } + if (AllowedProviders != otherRecord.AllowedProviders) + { + return false; + } + + if (ValueType != otherRecord.ValueType) + { + return false; + } + + if (!this.HasSameExtraProperties(otherRecord)) + { + return false; + } + + return true; + } + + public void Patch(FeatureDefinitionRecord otherRecord) + { + if (Name != otherRecord.Name) + { + Name = otherRecord.Name; + } + + if (GroupName != otherRecord.GroupName) + { + GroupName = otherRecord.GroupName; + } + + if (ParentName != otherRecord.ParentName) + { + ParentName = otherRecord.ParentName; + } + + if (DisplayName != otherRecord.DisplayName) + { + DisplayName = otherRecord.DisplayName; + } + + if (Description != otherRecord.Description) + { + Description = otherRecord.Description; + } + + if (DefaultValue != otherRecord.DefaultValue) + { + DefaultValue = otherRecord.DefaultValue; + } + + if (IsVisibleToClients != otherRecord.IsVisibleToClients) + { + IsVisibleToClients = otherRecord.IsVisibleToClients; + } + + if (IsAvailableToHost != otherRecord.IsAvailableToHost) + { + IsAvailableToHost = otherRecord.IsAvailableToHost; + } + + if (AllowedProviders != otherRecord.AllowedProviders) + { + AllowedProviders = otherRecord.AllowedProviders; + } + + if (ValueType != otherRecord.ValueType) + { + ValueType = otherRecord.ValueType; + } + + if (!this.HasSameExtraProperties(otherRecord)) + { + this.ExtraProperties.Clear(); + + foreach (var property in otherRecord.ExtraProperties) + { + this.ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionSerializer.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionSerializer.cs new file mode 100644 index 0000000000..70a2503b9c --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureDefinitionSerializer.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Features; +using Volo.Abp.Guids; +using Volo.Abp.Localization; +using Volo.Abp.Validation.StringValues; + +namespace Volo.Abp.FeatureManagement; + +public class FeatureDefinitionSerializer : IFeatureDefinitionSerializer, ITransientDependency +{ + protected IGuidGenerator GuidGenerator { get; } + protected ILocalizableStringSerializer LocalizableStringSerializer { get; } + protected StringValueTypeSerializer StringValueTypeSerializer { get; } + + public FeatureDefinitionSerializer(IGuidGenerator guidGenerator, ILocalizableStringSerializer localizableStringSerializer, StringValueTypeSerializer stringValueTypeSerializer) + { + GuidGenerator = guidGenerator; + LocalizableStringSerializer = localizableStringSerializer; + StringValueTypeSerializer = stringValueTypeSerializer; + } + + public async Task<(FeatureGroupDefinitionRecord[], FeatureDefinitionRecord[])> SerializeAsync(IEnumerable featureGroups) + { + var featureGroupRecords = new List(); + var featureRecords = new List(); + + foreach (var featureGroup in featureGroups) + { + featureGroupRecords.Add(await SerializeAsync(featureGroup)); + + foreach (var feature in featureGroup.GetFeaturesWithChildren()) + { + featureRecords.Add(await SerializeAsync(feature, featureGroup)); + } + } + + return (featureGroupRecords.ToArray(), featureRecords.ToArray()); + } + + public Task SerializeAsync(FeatureGroupDefinition featureGroup) + { + using (CultureHelper.Use(CultureInfo.InvariantCulture)) + { + var featureGroupRecord = new FeatureGroupDefinitionRecord( + GuidGenerator.Create(), + featureGroup.Name, + LocalizableStringSerializer.Serialize(featureGroup.DisplayName) + ); + + foreach (var property in featureGroup.Properties) + { + featureGroupRecord.SetProperty(property.Key, property.Value); + } + + return Task.FromResult(featureGroupRecord); + } + } + + public Task SerializeAsync(FeatureDefinition feature, FeatureGroupDefinition featureGroup) + { + using (CultureHelper.Use(CultureInfo.InvariantCulture)) + { + var featureRecord = new FeatureDefinitionRecord( + GuidGenerator.Create(), + featureGroup?.Name, + feature.Name, + feature.Parent?.Name, + LocalizableStringSerializer.Serialize(feature.DisplayName), + LocalizableStringSerializer.Serialize(feature.Description), + feature.DefaultValue, + feature.IsVisibleToClients, + feature.IsAvailableToHost, + SerializeProviders(feature.AllowedProviders), + SerializeStringValueType(feature.ValueType) + ); + + foreach (var property in feature.Properties) + { + featureRecord.SetProperty(property.Key, property.Value); + } + + return Task.FromResult(featureRecord); + } + } + + protected virtual string SerializeProviders(ICollection providers) + { + return providers.Any() + ? providers.JoinAsString(",") + : null; + } + + protected virtual string SerializeStringValueType(IStringValueType stringValueType) + { + return StringValueTypeSerializer.Serialize(stringValueType); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecord.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecord.cs new file mode 100644 index 0000000000..c40f74c2cd --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureGroupDefinitionRecord.cs @@ -0,0 +1,85 @@ +using System; +using Newtonsoft.Json; +using Volo.Abp.Data; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.FeatureManagement; + +public class FeatureGroupDefinitionRecord : BasicAggregateRoot, IHasExtraProperties +{ + /* Ignoring Id because it is different whenever we create an instance of + * this class, and we are using Json Serialize, than Hash to understand + * if feature definitions have changed (in StaticFeatureSaver.CalculateHash()). + */ + [JsonIgnore] //TODO: TODO: Use JSON modifier to ignore this property + public override Guid Id { get; protected set; } + + public string Name { get; set; } + + public string DisplayName { get; set; } + + public ExtraPropertyDictionary ExtraProperties { get; protected set; } + + public FeatureGroupDefinitionRecord() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public FeatureGroupDefinitionRecord( + Guid id, + string name, + string displayName) + : base(id) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), FeatureGroupDefinitionRecordConsts.MaxNameLength); + DisplayName = Check.NotNullOrWhiteSpace(displayName, nameof(displayName), FeatureGroupDefinitionRecordConsts.MaxDisplayNameLength);; + + + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + } + + public bool HasSameData(FeatureGroupDefinitionRecord otherRecord) + { + if (Name != otherRecord.Name) + { + return false; + } + + if (DisplayName != otherRecord.DisplayName) + { + return false; + } + + if (!this.HasSameExtraProperties(otherRecord)) + { + return false; + } + + return true; + } + + public void Patch(FeatureGroupDefinitionRecord otherRecord) + { + if (Name != otherRecord.Name) + { + Name = otherRecord.Name; + } + + if (DisplayName != otherRecord.DisplayName) + { + DisplayName = otherRecord.DisplayName; + } + + if (!this.HasSameExtraProperties(otherRecord)) + { + this.ExtraProperties.Clear(); + + foreach (var property in otherRecord.ExtraProperties) + { + this.ExtraProperties.Add(property.Key, property.Value); + } + } + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementOptions.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementOptions.cs index b04f4fbef2..a3bf5eefc5 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementOptions.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManagementOptions.cs @@ -9,6 +9,16 @@ public class FeatureManagementOptions public Dictionary ProviderPolicies { get; } + /// + /// Default: true. + /// + public bool SaveStaticFeaturesToDatabase { get; set; } = true; + + /// + /// Default: false. + /// + public bool IsDynamicFeatureStoreEnabled { get; set; } + public FeatureManagementOptions() { Providers = new TypeList(); 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 57db058216..59a0085d4c 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 @@ -87,7 +87,7 @@ public class FeatureManagementStore : IFeatureManagementStore, ITransientDepende string currentName, FeatureValueCacheItem currentCacheItem) { - var featureDefinitions = FeatureDefinitionManager.GetAll(); + var featureDefinitions = await FeatureDefinitionManager.GetAllAsync(); var featuresDictionary = (await FeatureValueRepository.GetListAsync(providerName, providerKey)) .ToDictionary(s => s.Name, s => s.Value); diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs index 36800a9a33..2c255071f7 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/FeatureManager.cs @@ -75,7 +75,7 @@ public class FeatureManager : IFeatureManager, ISingletonDependency { Check.NotNull(providerName, nameof(providerName)); - var featureDefinitions = FeatureDefinitionManager.GetAll(); + var featureDefinitions = await FeatureDefinitionManager.GetAllAsync(); var providers = Enumerable.Reverse(Providers).SkipWhile(c => c.Name != providerName); if (!fallback) @@ -130,7 +130,7 @@ public class FeatureManager : IFeatureManager, ISingletonDependency Check.NotNull(name, nameof(name)); Check.NotNull(providerName, nameof(providerName)); - var feature = FeatureDefinitionManager.Get(name); + var feature = await FeatureDefinitionManager.GetAsync(name); if (feature.ValueType?.Validator.IsValid(value) == false) { @@ -186,7 +186,7 @@ public class FeatureManager : IFeatureManager, ISingletonDependency string providerKey, bool fallback = true) //TODO: Fallback is not used { - var feature = FeatureDefinitionManager.Get(name); + var feature = await FeatureDefinitionManager.GetAsync(name); var providers = Enumerable .Reverse(Providers); diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IDynamicFeatureDefinitionStoreInMemoryCache.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IDynamicFeatureDefinitionStoreInMemoryCache.cs new file mode 100644 index 0000000000..b51b63d1f8 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IDynamicFeatureDefinitionStoreInMemoryCache.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Features; + +namespace Volo.Abp.FeatureManagement; + +public interface IDynamicFeatureDefinitionStoreInMemoryCache +{ + string CacheStamp { get; set; } + + SemaphoreSlim SyncSemaphore { get; } + + DateTime? LastCheckTime { get; set; } + + Task FillAsync( + List featureGroupRecords, + List featureRecords); + + FeatureDefinition GetFeatureOrNull(string name); + + IReadOnlyList GetFeatures(); + + IReadOnlyList GetGroups(); +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionRecordRepository.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionRecordRepository.cs new file mode 100644 index 0000000000..35144df2f0 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionRecordRepository.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.FeatureManagement; + +public interface IFeatureDefinitionRecordRepository : IBasicRepository +{ + +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionSerializer.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionSerializer.cs new file mode 100644 index 0000000000..96a8f542ab --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureDefinitionSerializer.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Features; + +namespace Volo.Abp.FeatureManagement; + +public interface IFeatureDefinitionSerializer +{ + Task<(FeatureGroupDefinitionRecord[], FeatureDefinitionRecord[])> SerializeAsync(IEnumerable featureGroups); + + Task SerializeAsync(FeatureGroupDefinition featureGroup); + + Task SerializeAsync(FeatureDefinition feature, [CanBeNull] FeatureGroupDefinition featureGroup); +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureGroupDefinitionRecordRepository.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureGroupDefinitionRecordRepository.cs new file mode 100644 index 0000000000..e48e8bedc2 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IFeatureGroupDefinitionRecordRepository.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace Volo.Abp.FeatureManagement; + +public interface IFeatureGroupDefinitionRecordRepository : IBasicRepository +{ + +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IStaticFeatureSaver.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IStaticFeatureSaver.cs new file mode 100644 index 0000000000..d72d48525e --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/IStaticFeatureSaver.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.FeatureManagement; + +public interface IStaticFeatureSaver +{ + Task SaveAsync(); +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureSaver.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureSaver.cs new file mode 100644 index 0000000000..8786f9ddb6 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StaticFeatureSaver.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DistributedLocking; +using Volo.Abp.Features; +using Volo.Abp.Threading; +using Volo.Abp.Uow; + +namespace Volo.Abp.FeatureManagement; + +public class StaticFeatureSaver : IStaticFeatureSaver, ITransientDependency +{ + protected IStaticFeatureDefinitionStore StaticStore { get; } + protected IFeatureGroupDefinitionRecordRepository FeatureGroupRepository { get; } + protected IFeatureDefinitionRecordRepository FeatureRepository { get; } + protected IFeatureDefinitionSerializer FeatureSerializer { get; } + protected IDistributedCache Cache { get; } + protected IApplicationNameAccessor ApplicationNameAccessor { get; } + protected IAbpDistributedLock DistributedLock { get; } + protected AbpFeatureOptions FeatureOptions { get; } + protected ICancellationTokenProvider CancellationTokenProvider { get; } + protected AbpDistributedCacheOptions CacheOptions { get; } + + public StaticFeatureSaver( + IStaticFeatureDefinitionStore staticStore, + IFeatureGroupDefinitionRecordRepository featureGroupRepository, + IFeatureDefinitionRecordRepository featureRepository, + IFeatureDefinitionSerializer featureSerializer, + IDistributedCache cache, + IOptions cacheOptions, + IApplicationNameAccessor applicationNameAccessor, + IAbpDistributedLock distributedLock, + IOptions featureManagementOptions, + ICancellationTokenProvider cancellationTokenProvider) + { + StaticStore = staticStore; + FeatureGroupRepository = featureGroupRepository; + FeatureRepository = featureRepository; + FeatureSerializer = featureSerializer; + Cache = cache; + ApplicationNameAccessor = applicationNameAccessor; + DistributedLock = distributedLock; + CancellationTokenProvider = cancellationTokenProvider; + FeatureOptions = featureManagementOptions.Value; + CacheOptions = cacheOptions.Value; + } + + [UnitOfWork] + public virtual async Task SaveAsync() + { + await using var applicationLockHandle = await DistributedLock.TryAcquireAsync( + GetApplicationDistributedLockKey() + ); + + if (applicationLockHandle == null) + { + /* Another application instance is already doing it */ + return; + } + + /* NOTE: This can be further optimized by using 4 cache values for: + * Groups, features, deleted groups and deleted features. + * But the code would be more complex. This is enough for now. + */ + + var cacheKey = GetApplicationHashCacheKey(); + var cachedHash = await Cache.GetStringAsync(cacheKey, CancellationTokenProvider.Token); + + var (featureGroupRecords, featureRecords) = await FeatureSerializer.SerializeAsync( + await StaticStore.GetGroupsAsync() + ); + + var currentHash = CalculateHash( + featureGroupRecords, + featureRecords, + FeatureOptions.DeletedFeatureGroups, + FeatureOptions.DeletedFeatures + ); + + if (cachedHash == currentHash) + { + return; + } + + await using (var commonLockHandle = await DistributedLock.TryAcquireAsync( + GetCommonDistributedLockKey(), + TimeSpan.FromMinutes(5))) + { + if (commonLockHandle == null) + { + /* It will re-try */ + throw new AbpException("Could not acquire distributed lock for saving static features!"); + } + + var hasChangesInGroups = await UpdateChangedFeatureGroupsAsync(featureGroupRecords); + var hasChangesInFeatures = await UpdateChangedFeaturesAsync(featureRecords); + + if (hasChangesInGroups ||hasChangesInFeatures) + { + await Cache.SetStringAsync( + GetCommonStampCacheKey(), + Guid.NewGuid().ToString(), + new DistributedCacheEntryOptions { + SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable? + }, + CancellationTokenProvider.Token + ); + } + } + + await Cache.SetStringAsync( + cacheKey, + currentHash, + new DistributedCacheEntryOptions { + SlidingExpiration = TimeSpan.FromDays(30) //TODO: Make it configurable? + }, + CancellationTokenProvider.Token + ); + } + + private async Task UpdateChangedFeatureGroupsAsync( + IEnumerable featureGroupRecords) + { + var newRecords = new List(); + var changedRecords = new List(); + + var featureGroupRecordsInDatabase = (await FeatureGroupRepository.GetListAsync()) + .ToDictionary(x => x.Name); + + foreach (var featureGroupRecord in featureGroupRecords) + { + var featureGroupRecordInDatabase = featureGroupRecordsInDatabase.GetOrDefault(featureGroupRecord.Name); + if (featureGroupRecordInDatabase == null) + { + /* New group */ + newRecords.Add(featureGroupRecord); + continue; + } + + if (featureGroupRecord.HasSameData(featureGroupRecordInDatabase)) + { + /* Not changed */ + continue; + } + + /* Changed */ + featureGroupRecordInDatabase.Patch(featureGroupRecord); + changedRecords.Add(featureGroupRecordInDatabase); + } + + /* Deleted */ + var deletedRecords = FeatureOptions.DeletedFeatureGroups.Any() + ? featureGroupRecordsInDatabase.Values + .Where(x => FeatureOptions.DeletedFeatureGroups.Contains(x.Name)) + .ToArray() + : Array.Empty(); + + if (newRecords.Any()) + { + await FeatureGroupRepository.InsertManyAsync(newRecords); + } + + if (changedRecords.Any()) + { + await FeatureGroupRepository.UpdateManyAsync(changedRecords); + } + + if (deletedRecords.Any()) + { + await FeatureGroupRepository.DeleteManyAsync(deletedRecords); + } + + return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); + } + + private async Task UpdateChangedFeaturesAsync( + IEnumerable featureRecords) + { + var newRecords = new List(); + var changedRecords = new List(); + + var featureRecordsInDatabase = (await FeatureRepository.GetListAsync()) + .ToDictionary(x => x.Name); + + foreach (var featureRecord in featureRecords) + { + var featureRecordInDatabase = featureRecordsInDatabase.GetOrDefault(featureRecord.Name); + if (featureRecordInDatabase == null) + { + /* New group */ + newRecords.Add(featureRecord); + continue; + } + + if (featureRecord.HasSameData(featureRecordInDatabase)) + { + /* Not changed */ + continue; + } + + /* Changed */ + featureRecordInDatabase.Patch(featureRecord); + changedRecords.Add(featureRecordInDatabase); + } + + /* Deleted */ + var deletedRecords = new List(); + + if (FeatureOptions.DeletedFeatures.Any()) + { + deletedRecords.AddRange( + featureRecordsInDatabase.Values + .Where(x => FeatureOptions.DeletedFeatures.Contains(x.Name)) + ); + } + + if (FeatureOptions.DeletedFeatureGroups.Any()) + { + deletedRecords.AddIfNotContains( + featureRecordsInDatabase.Values + .Where(x => FeatureOptions.DeletedFeatureGroups.Contains(x.GroupName)) + ); + } + + if (newRecords.Any()) + { + await FeatureRepository.InsertManyAsync(newRecords); + } + + if (changedRecords.Any()) + { + await FeatureRepository.UpdateManyAsync(changedRecords); + } + + if (deletedRecords.Any()) + { + await FeatureRepository.DeleteManyAsync(deletedRecords); + } + + return newRecords.Any() || changedRecords.Any() || deletedRecords.Any(); + } + + private string GetApplicationDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationNameAccessor.ApplicationName}_AbpFeatureUpdateLock"; + } + + private string GetCommonDistributedLockKey() + { + return $"{CacheOptions.KeyPrefix}_Common_AbpFeatureUpdateLock"; + } + + private string GetApplicationHashCacheKey() + { + return $"{CacheOptions.KeyPrefix}_{ApplicationNameAccessor.ApplicationName}_AbpFeaturesHash"; + } + + private string GetCommonStampCacheKey() + { + return $"{CacheOptions.KeyPrefix}_AbpInMemoryFeatureCacheStamp"; + } + + private static string CalculateHash( + FeatureGroupDefinitionRecord[] featureGroupRecords, + FeatureDefinitionRecord[] featureRecords, + IEnumerable deletedFeatureGroups, + IEnumerable deletedFeatures) + { + var stringBuilder = new StringBuilder(); + + stringBuilder.Append("FeatureGroupRecords:"); + stringBuilder.AppendLine(JsonSerializer.Serialize(featureGroupRecords)); + + stringBuilder.Append("FeatureRecords:"); + stringBuilder.AppendLine(JsonSerializer.Serialize(featureRecords)); + + stringBuilder.Append("DeletedFeatureGroups:"); + stringBuilder.AppendLine(deletedFeatureGroups.JoinAsString(",")); + + stringBuilder.Append("DeletedFeature:"); + stringBuilder.Append(deletedFeatures.JoinAsString(",")); + + return stringBuilder + .ToString() + .ToMd5(); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StringValueTypeSerializer.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StringValueTypeSerializer.cs new file mode 100644 index 0000000000..f16ffda192 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Domain/Volo/Abp/FeatureManagement/StringValueTypeSerializer.cs @@ -0,0 +1,25 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; +using Volo.Abp.Validation.StringValues; + +namespace Volo.Abp.FeatureManagement; + +public class StringValueTypeSerializer : ITransientDependency +{ + protected IJsonSerializer JsonSerializer { get; } + + public StringValueTypeSerializer(IJsonSerializer jsonSerializer) + { + JsonSerializer = jsonSerializer; + } + + public virtual string Serialize(IStringValueType stringValueType) + { + return JsonSerializer.Serialize(stringValueType); + } + + public virtual IStringValueType Deserialize(string value) + { + return JsonSerializer.Deserialize(value); + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreModule.cs index 716410022f..b342831d92 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/AbpFeatureManagementEntityFrameworkCoreModule.cs @@ -1,12 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Json; using Volo.Abp.Modularity; namespace Volo.Abp.FeatureManagement.EntityFrameworkCore; [DependsOn( typeof(AbpFeatureManagementDomainModule), - typeof(AbpEntityFrameworkCoreModule) + typeof(AbpEntityFrameworkCoreModule), + typeof(AbpJsonModule) )] public class AbpFeatureManagementEntityFrameworkCoreModule : AbpModule { @@ -14,6 +16,8 @@ public class AbpFeatureManagementEntityFrameworkCoreModule : AbpModule { context.Services.AddAbpDbContext(options => { + options.AddRepository(); + options.AddRepository(); options.AddDefaultRepositories(); options.AddRepository(); diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureDefinitionRecordRepository.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureDefinitionRecordRepository.cs new file mode 100644 index 0000000000..91648af33b --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureDefinitionRecordRepository.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.FeatureManagement.EntityFrameworkCore; + +public class EfCoreFeatureDefinitionRecordRepository : + EfCoreRepository, + IFeatureDefinitionRecordRepository +{ + public EfCoreFeatureDefinitionRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureGroupDefinitionRecordRepository.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureGroupDefinitionRecordRepository.cs new file mode 100644 index 0000000000..81b37e8452 --- /dev/null +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/EfCoreFeatureGroupDefinitionRecordRepository.cs @@ -0,0 +1,16 @@ +using System; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace Volo.Abp.FeatureManagement.EntityFrameworkCore; + +public class EfCoreFeatureGroupDefinitionRecordRepository : + EfCoreRepository, + IFeatureGroupDefinitionRecordRepository +{ + public EfCoreFeatureGroupDefinitionRecordRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } +} diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContext.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContext.cs index 1999a11984..0bc5fdee54 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContext.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContext.cs @@ -9,6 +9,10 @@ namespace Volo.Abp.FeatureManagement.EntityFrameworkCore; [ConnectionStringName(AbpFeatureManagementDbProperties.ConnectionStringName)] public class FeatureManagementDbContext : AbpDbContext, IFeatureManagementDbContext { + public DbSet FeatureGroups { get; set; } + + public DbSet Features { get; set; } + public DbSet FeatureValues { get; set; } public FeatureManagementDbContext(DbContextOptions options) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs index 97a9ba83d7..1ed23241dc 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/FeatureManagementDbContextModelCreatingExtensions.cs @@ -26,7 +26,41 @@ public static class FeatureManagementDbContextModelCreatingExtensions b.Property(x => x.ProviderName).HasMaxLength(FeatureValueConsts.MaxProviderNameLength); b.Property(x => x.ProviderKey).HasMaxLength(FeatureValueConsts.MaxProviderKeyLength); - b.HasIndex(x => new { x.Name, x.ProviderName, x.ProviderKey }).IsUnique(true); + b.HasIndex(x => new { x.Name, x.ProviderName, x.ProviderKey }).IsUnique(); + + b.ApplyObjectExtensionMappings(); + }); + builder.Entity(b => + { + b.ToTable(AbpFeatureManagementDbProperties.DbTablePrefix + "FeatureGroups", AbpFeatureManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.Name).HasMaxLength(FeatureGroupDefinitionRecordConsts.MaxNameLength).IsRequired(); + b.Property(x => x.DisplayName).HasMaxLength(FeatureGroupDefinitionRecordConsts.MaxDisplayNameLength).IsRequired(); + + b.HasIndex(x => new { x.Name }).IsUnique(); + + b.ApplyObjectExtensionMappings(); + }); + + builder.Entity(b => + { + b.ToTable(AbpFeatureManagementDbProperties.DbTablePrefix + "Features", AbpFeatureManagementDbProperties.DbSchema); + + b.ConfigureByConvention(); + + b.Property(x => x.GroupName).HasMaxLength(FeatureGroupDefinitionRecordConsts.MaxNameLength).IsRequired(); + b.Property(x => x.Name).HasMaxLength(FeatureDefinitionRecordConsts.MaxNameLength).IsRequired(); + b.Property(x => x.ParentName).HasMaxLength(FeatureDefinitionRecordConsts.MaxNameLength); + b.Property(x => x.DisplayName).HasMaxLength(FeatureDefinitionRecordConsts.MaxDisplayNameLength).IsRequired(); + b.Property(x => x.Description).HasMaxLength(FeatureDefinitionRecordConsts.MaxDescriptionLength); + b.Property(x => x.DefaultValue).HasMaxLength(FeatureDefinitionRecordConsts.MaxDefaultValueLength); + b.Property(x => x.AllowedProviders).HasMaxLength(FeatureDefinitionRecordConsts.MaxAllowedProvidersLength); + b.Property(x => x.ValueType).HasMaxLength(FeatureDefinitionRecordConsts.MaxValueTypeLength); + + b.HasIndex(x => new { x.Name }).IsUnique(); + b.HasIndex(x => new { x.GroupName }); b.ApplyObjectExtensionMappings(); }); diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/IFeatureManagementDbContext.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/IFeatureManagementDbContext.cs index 7848f56cab..dce762b2f2 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/IFeatureManagementDbContext.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.EntityFrameworkCore/Volo/Abp/FeatureManagement/EntityFrameworkCore/IFeatureManagementDbContext.cs @@ -9,5 +9,9 @@ namespace Volo.Abp.FeatureManagement.EntityFrameworkCore; [ConnectionStringName(AbpFeatureManagementDbProperties.ConnectionStringName)] public interface IFeatureManagementDbContext : IEfCoreDbContext { + DbSet FeatureGroups { get; } + + DbSet Features { get; } + DbSet FeatureValues { get; } } diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/AbpFeatureManagementHttpApiModule.cs b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/AbpFeatureManagementHttpApiModule.cs index 2a0b33d818..a331a46455 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/AbpFeatureManagementHttpApiModule.cs +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.HttpApi/Volo/Abp/FeatureManagement/AbpFeatureManagementHttpApiModule.cs @@ -32,7 +32,7 @@ public class AbpFeatureManagementHttpApiModule : AbpModule .AddBaseTypes(typeof(AbpUiResource)); }); - var contractsOptions = context.Services.ExecutePreConfiguredActions(); + var contractsOptions = context.Services.ExecutePreConfiguredActions(); Configure(options => { options.JsonSerializerOptions.Converters.AddIfNotContains(new StringValueTypeJsonConverter(contractsOptions)); diff --git a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs index 28b8e55052..be4b431b82 100644 --- a/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs +++ b/modules/feature-management/test/Volo.Abp.FeatureManagement.Application.Tests/Volo/Abp/FeatureManagement/StringValueJsonConverter_Tests.cs @@ -20,7 +20,7 @@ public abstract class StringValueJsonConverter_Tests : FeatureManagementApplicat protected override void BeforeAddApplication(IServiceCollection services) { - services.PreConfigure(options => + services.PreConfigure(options => { options.ValueValidatorFactory.Add(new ValueValidatorFactory("URL")); });