mirror of https://github.com/abpframework/abp.git
committed by
GitHub
98 changed files with 3825 additions and 300 deletions
@ -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); |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Features; |
||||
|
|
||||
|
public interface IDynamicFeatureDefinitionStore |
||||
|
{ |
||||
|
Task<FeatureDefinition> GetOrNullAsync(string name); |
||||
|
|
||||
|
Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync(); |
||||
|
|
||||
|
Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync(); |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Features; |
||||
|
|
||||
|
public interface IStaticFeatureDefinitionStore |
||||
|
{ |
||||
|
Task<FeatureDefinition> GetOrNullAsync(string name); |
||||
|
|
||||
|
Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync(); |
||||
|
|
||||
|
Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync(); |
||||
|
} |
||||
@ -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<FeatureDefinition> CachedFeatureResult = Task.FromResult((FeatureDefinition)null); |
||||
|
|
||||
|
private static readonly Task<IReadOnlyList<FeatureDefinition>> CachedFeaturesResult = |
||||
|
Task.FromResult((IReadOnlyList<FeatureDefinition>)Array.Empty<FeatureDefinition>().ToImmutableList()); |
||||
|
|
||||
|
private static readonly Task<IReadOnlyList<FeatureGroupDefinition>> CachedGroupsResult = |
||||
|
Task.FromResult((IReadOnlyList<FeatureGroupDefinition>)Array.Empty<FeatureGroupDefinition>().ToImmutableList()); |
||||
|
|
||||
|
public Task<FeatureDefinition> GetOrNullAsync(string name) |
||||
|
{ |
||||
|
return CachedFeatureResult; |
||||
|
} |
||||
|
|
||||
|
public Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync() |
||||
|
{ |
||||
|
return CachedFeaturesResult; |
||||
|
} |
||||
|
|
||||
|
public Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync() |
||||
|
{ |
||||
|
return CachedGroupsResult; |
||||
|
} |
||||
|
} |
||||
@ -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<string, FeatureGroupDefinition> FeatureGroupDefinitions => _lazyFeatureGroupDefinitions.Value; |
||||
|
private readonly Lazy<Dictionary<string, FeatureGroupDefinition>> _lazyFeatureGroupDefinitions; |
||||
|
|
||||
|
protected IDictionary<string, FeatureDefinition> FeatureDefinitions => _lazyFeatureDefinitions.Value; |
||||
|
private readonly Lazy<Dictionary<string, FeatureDefinition>> _lazyFeatureDefinitions; |
||||
|
|
||||
|
protected AbpFeatureOptions Options { get; } |
||||
|
|
||||
|
private readonly IServiceProvider _serviceProvider; |
||||
|
|
||||
|
public StaticFeatureDefinitionStore( |
||||
|
IOptions<AbpFeatureOptions> options, |
||||
|
IServiceProvider serviceProvider) |
||||
|
{ |
||||
|
_serviceProvider = serviceProvider; |
||||
|
Options = options.Value; |
||||
|
|
||||
|
_lazyFeatureDefinitions = new Lazy<Dictionary<string, FeatureDefinition>>( |
||||
|
CreateFeatureDefinitions, |
||||
|
isThreadSafe: true |
||||
|
); |
||||
|
|
||||
|
_lazyFeatureGroupDefinitions = new Lazy<Dictionary<string, FeatureGroupDefinition>>( |
||||
|
CreateFeatureGroupDefinitions, |
||||
|
isThreadSafe: true |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<FeatureDefinition> 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<string, FeatureDefinition> CreateFeatureDefinitions() |
||||
|
{ |
||||
|
var features = new Dictionary<string, FeatureDefinition>(); |
||||
|
|
||||
|
foreach (var groupDefinition in FeatureGroupDefinitions.Values) |
||||
|
{ |
||||
|
foreach (var feature in groupDefinition.Features) |
||||
|
{ |
||||
|
AddFeatureToDictionaryRecursively(features, feature); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return features; |
||||
|
} |
||||
|
|
||||
|
protected virtual void AddFeatureToDictionaryRecursively( |
||||
|
Dictionary<string, FeatureDefinition> 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<string, FeatureGroupDefinition> 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<FeatureDefinition> GetOrNullAsync(string name) |
||||
|
{ |
||||
|
return Task.FromResult(FeatureDefinitions.GetOrDefault(name)); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync() |
||||
|
{ |
||||
|
return Task.FromResult<IReadOnlyList<FeatureDefinition>>(FeatureDefinitions.Values.ToList()); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync() |
||||
|
{ |
||||
|
return Task.FromResult<IReadOnlyList<FeatureGroupDefinition>>(FeatureGroupDefinitions.Values.ToList()); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Authorization.Permissions; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.Authorization; |
||||
|
|
||||
|
public class StaticPermissionDefinitionStore_Tests : AuthorizationTestBase |
||||
|
{ |
||||
|
private readonly IStaticPermissionDefinitionStore _store; |
||||
|
|
||||
|
public StaticPermissionDefinitionStore_Tests() |
||||
|
{ |
||||
|
_store = GetRequiredService<IStaticPermissionDefinitionStore>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetOrNullAsync() |
||||
|
{ |
||||
|
var permission = await _store.GetOrNullAsync("MyPermission1"); |
||||
|
permission.ShouldNotBeNull(); |
||||
|
permission.Name.ShouldBe("MyPermission1"); |
||||
|
permission.StateCheckers.ShouldContain(x => x.GetType() == typeof(TestRequireEditionPermissionSimpleStateChecker)); |
||||
|
|
||||
|
permission = await _store.GetOrNullAsync("NotExists"); |
||||
|
permission.ShouldBeNull(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetPermissionsAsync() |
||||
|
{ |
||||
|
var permissions = await _store.GetPermissionsAsync(); |
||||
|
permissions.ShouldContain(x => x.Name == "MyAuthorizedService1"); |
||||
|
permissions.ShouldContain(x => x.Name == "MyPermission1"); |
||||
|
permissions.ShouldContain(x => x.Name == "MyPermission2"); |
||||
|
permissions.ShouldContain(x => x.Name == "MyPermission3"); |
||||
|
permissions.ShouldContain(x => x.Name == "MyPermission4"); |
||||
|
permissions.ShouldContain(x => x.Name == "MyPermission5"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task GetGroupsAsync() |
||||
|
{ |
||||
|
var groups = await _store.GetGroupsAsync(); |
||||
|
groups.ShouldNotContain(x => x.Name == "TestGetGroup"); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
4
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs
4
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/NewtonsoftStringValueTypeJsonConverter.cs
0
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs
0
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/SelectionStringValueItemSourceJsonConverter.cs
4
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs
4
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/StringValueTypeJsonConverter.cs
4
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs
4
modules/feature-management/src/Volo.Abp.FeatureManagement.Application.Contracts/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.cs → modules/feature-management/src/Volo.Abp.FeatureManagement.Domain.Shared/Volo/Abp/FeatureManagement/JsonConverters/ValueValidatorJsonConverter.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<AbpDistributedCacheOptions> cacheOptions, |
||||
|
IOptions<FeatureManagementOptions> 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<FeatureDefinition> GetOrNullAsync(string name) |
||||
|
{ |
||||
|
if (!FeatureManagementOptions.IsDynamicFeatureStoreEnabled) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
using (await StoreCache.SyncSemaphore.LockAsync()) |
||||
|
{ |
||||
|
await EnsureCacheIsUptoDateAsync(); |
||||
|
return StoreCache.GetFeatureOrNull(name); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<IReadOnlyList<FeatureDefinition>> GetFeaturesAsync() |
||||
|
{ |
||||
|
if (!FeatureManagementOptions.IsDynamicFeatureStoreEnabled) |
||||
|
{ |
||||
|
return Array.Empty<FeatureDefinition>(); |
||||
|
} |
||||
|
|
||||
|
using (await StoreCache.SyncSemaphore.LockAsync()) |
||||
|
{ |
||||
|
await EnsureCacheIsUptoDateAsync(); |
||||
|
return StoreCache.GetFeatures().ToImmutableList(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync() |
||||
|
{ |
||||
|
if (!FeatureManagementOptions.IsDynamicFeatureStoreEnabled) |
||||
|
{ |
||||
|
return Array.Empty<FeatureGroupDefinition>(); |
||||
|
} |
||||
|
|
||||
|
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<string> 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"; |
||||
|
} |
||||
|
} |
||||
@ -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<string, FeatureGroupDefinition> FeatureGroupDefinitions { get; } |
||||
|
protected IDictionary<string, FeatureDefinition> 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<string, FeatureGroupDefinition>(); |
||||
|
FeatureDefinitions = new Dictionary<string, FeatureDefinition>(); |
||||
|
} |
||||
|
|
||||
|
public Task FillAsync( |
||||
|
List<FeatureGroupDefinitionRecord> featureGroupRecords, |
||||
|
List<FeatureDefinitionRecord> 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<FeatureDefinition> GetFeatures() |
||||
|
{ |
||||
|
return FeatureDefinitions.Values.ToList(); |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<FeatureGroupDefinition> GetGroups() |
||||
|
{ |
||||
|
return FeatureGroupDefinitions.Values.ToList(); |
||||
|
} |
||||
|
|
||||
|
private void AddFeatureRecursively(ICanCreateChildFeature featureContainer, |
||||
|
FeatureDefinitionRecord featureRecord, |
||||
|
List<FeatureDefinitionRecord> 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<Guid>, 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; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Comma separated list of provider names.
|
||||
|
/// </summary>
|
||||
|
public string AllowedProviders { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Serialized string to store info about the ValueType.
|
||||
|
/// </summary>
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<FeatureGroupDefinition> featureGroups) |
||||
|
{ |
||||
|
var featureGroupRecords = new List<FeatureGroupDefinitionRecord>(); |
||||
|
var featureRecords = new List<FeatureDefinitionRecord>(); |
||||
|
|
||||
|
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<FeatureGroupDefinitionRecord> 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<FeatureDefinitionRecord> 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<string> providers) |
||||
|
{ |
||||
|
return providers.Any() |
||||
|
? providers.JoinAsString(",") |
||||
|
: null; |
||||
|
} |
||||
|
|
||||
|
protected virtual string SerializeStringValueType(IStringValueType stringValueType) |
||||
|
{ |
||||
|
return StringValueTypeSerializer.Serialize(stringValueType); |
||||
|
} |
||||
|
} |
||||
@ -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<Guid>, 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<FeatureGroupDefinitionRecord> featureGroupRecords, |
||||
|
List<FeatureDefinitionRecord> featureRecords); |
||||
|
|
||||
|
FeatureDefinition GetFeatureOrNull(string name); |
||||
|
|
||||
|
IReadOnlyList<FeatureDefinition> GetFeatures(); |
||||
|
|
||||
|
IReadOnlyList<FeatureGroupDefinition> GetGroups(); |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement; |
||||
|
|
||||
|
public interface IFeatureDefinitionRecordRepository : IBasicRepository<FeatureDefinitionRecord, Guid> |
||||
|
{ |
||||
|
Task<FeatureDefinitionRecord> FindByNameAsync( |
||||
|
string name, |
||||
|
CancellationToken cancellationToken = default); |
||||
|
} |
||||
@ -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<FeatureGroupDefinition> featureGroups); |
||||
|
|
||||
|
Task<FeatureGroupDefinitionRecord> SerializeAsync(FeatureGroupDefinition featureGroup); |
||||
|
|
||||
|
Task<FeatureDefinitionRecord> SerializeAsync(FeatureDefinition feature, [CanBeNull] FeatureGroupDefinition featureGroup); |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Domain.Repositories; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement; |
||||
|
|
||||
|
public interface IFeatureGroupDefinitionRecordRepository : IBasicRepository<FeatureGroupDefinitionRecord, Guid> |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement; |
||||
|
|
||||
|
public interface IStaticFeatureSaver |
||||
|
{ |
||||
|
Task SaveAsync(); |
||||
|
} |
||||
@ -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<AbpDistributedCacheOptions> cacheOptions, |
||||
|
IApplicationNameAccessor applicationNameAccessor, |
||||
|
IAbpDistributedLock distributedLock, |
||||
|
IOptions<AbpFeatureOptions> 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<bool> UpdateChangedFeatureGroupsAsync( |
||||
|
IEnumerable<FeatureGroupDefinitionRecord> featureGroupRecords) |
||||
|
{ |
||||
|
var newRecords = new List<FeatureGroupDefinitionRecord>(); |
||||
|
var changedRecords = new List<FeatureGroupDefinitionRecord>(); |
||||
|
|
||||
|
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<FeatureGroupDefinitionRecord>(); |
||||
|
|
||||
|
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<bool> UpdateChangedFeaturesAsync( |
||||
|
IEnumerable<FeatureDefinitionRecord> featureRecords) |
||||
|
{ |
||||
|
var newRecords = new List<FeatureDefinitionRecord>(); |
||||
|
var changedRecords = new List<FeatureDefinitionRecord>(); |
||||
|
|
||||
|
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<FeatureDefinitionRecord>(); |
||||
|
|
||||
|
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<string> deletedFeatureGroups, |
||||
|
IEnumerable<string> 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(); |
||||
|
} |
||||
|
} |
||||
@ -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<IStringValueType>(value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.EntityFrameworkCore; |
||||
|
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; |
||||
|
using Volo.Abp.EntityFrameworkCore; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement.EntityFrameworkCore; |
||||
|
|
||||
|
public class EfCoreFeatureDefinitionRecordRepository : |
||||
|
EfCoreRepository<IFeatureManagementDbContext, FeatureDefinitionRecord, Guid>, |
||||
|
IFeatureDefinitionRecordRepository |
||||
|
{ |
||||
|
public EfCoreFeatureDefinitionRecordRepository( |
||||
|
IDbContextProvider<IFeatureManagementDbContext> dbContextProvider) |
||||
|
: base(dbContextProvider) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<FeatureDefinitionRecord> FindByNameAsync(string name, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
return await (await GetDbSetAsync()) |
||||
|
.OrderBy(x => x.Id) |
||||
|
.FirstOrDefaultAsync(r => r.Name == name, cancellationToken); |
||||
|
} |
||||
|
} |
||||
@ -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<IFeatureManagementDbContext, FeatureGroupDefinitionRecord, Guid>, |
||||
|
IFeatureGroupDefinitionRecordRepository |
||||
|
{ |
||||
|
public EfCoreFeatureGroupDefinitionRecordRepository( |
||||
|
IDbContextProvider<IFeatureManagementDbContext> dbContextProvider) |
||||
|
: base(dbContextProvider) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Driver.Linq; |
||||
|
using Volo.Abp.Domain.Repositories.MongoDB; |
||||
|
using Volo.Abp.MongoDB; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement.MongoDB; |
||||
|
|
||||
|
public class MongoFeatureDefinitionRecordRepository : |
||||
|
MongoDbRepository<IFeatureManagementMongoDbContext, FeatureDefinitionRecord, Guid>, |
||||
|
IFeatureDefinitionRecordRepository |
||||
|
{ |
||||
|
public MongoFeatureDefinitionRecordRepository( |
||||
|
IMongoDbContextProvider<IFeatureManagementMongoDbContext> dbContextProvider) |
||||
|
: base(dbContextProvider) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public virtual async Task<FeatureDefinitionRecord> FindByNameAsync(string name, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
cancellationToken = GetCancellationToken(cancellationToken); |
||||
|
return await (await GetMongoQueryableAsync(cancellationToken)) |
||||
|
.OrderBy(x => x.Id) |
||||
|
.FirstOrDefaultAsync( |
||||
|
s => s.Name == name, |
||||
|
cancellationToken |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.Domain.Repositories.MongoDB; |
||||
|
using Volo.Abp.MongoDB; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement.MongoDB; |
||||
|
|
||||
|
public class MongoFeatureGroupDefinitionRecordRepository : |
||||
|
MongoDbRepository<IFeatureManagementMongoDbContext, FeatureGroupDefinitionRecord, Guid>, |
||||
|
IFeatureGroupDefinitionRecordRepository |
||||
|
{ |
||||
|
public MongoFeatureGroupDefinitionRecordRepository( |
||||
|
IMongoDbContextProvider<IFeatureManagementMongoDbContext> dbContextProvider) |
||||
|
: base(dbContextProvider) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,87 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.FeatureManagement.Localization; |
||||
|
using Volo.Abp.Features; |
||||
|
using Volo.Abp.Localization; |
||||
|
using Volo.Abp.Validation.StringValues; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement; |
||||
|
|
||||
|
public class FeatureDefinitionSerializer_Tests : FeatureManagementDomainTestBase |
||||
|
{ |
||||
|
private readonly IFeatureDefinitionSerializer _serializer; |
||||
|
|
||||
|
public FeatureDefinitionSerializer_Tests() |
||||
|
{ |
||||
|
_serializer = GetRequiredService<IFeatureDefinitionSerializer>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Serialize_Feature_Group_Definition() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
|
||||
|
var context = new FeatureDefinitionContext(); |
||||
|
var group1 = CreateFeatureGroup1(context); |
||||
|
|
||||
|
// Act
|
||||
|
|
||||
|
var featureGroupRecord = await _serializer.SerializeAsync(group1); |
||||
|
|
||||
|
//Assert
|
||||
|
|
||||
|
featureGroupRecord.Name.ShouldBe("Group1"); |
||||
|
featureGroupRecord.DisplayName.ShouldBe("F:Group one"); |
||||
|
featureGroupRecord.GetProperty("CustomProperty1").ShouldBe("CustomValue1"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Serialize_Complex_Feature_Definition() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
|
||||
|
var context = new FeatureDefinitionContext(); |
||||
|
var group1 = CreateFeatureGroup1(context); |
||||
|
var feature1 = group1.AddFeature( |
||||
|
"Feature1", |
||||
|
"default", |
||||
|
new LocalizableString(typeof(AbpFeatureManagementResource), "Feature1"), |
||||
|
new LocalizableString(typeof(AbpFeatureManagementResource), "Feature1"), |
||||
|
new ToggleStringValueType(), |
||||
|
isVisibleToClients: true |
||||
|
) |
||||
|
.WithProviders("ProviderA", "ProviderB") |
||||
|
.WithProperty("CustomProperty2", "CustomValue2"); |
||||
|
|
||||
|
// Act
|
||||
|
|
||||
|
var featureRecord = await _serializer.SerializeAsync( |
||||
|
feature1, |
||||
|
group1 |
||||
|
); |
||||
|
|
||||
|
//Assert
|
||||
|
|
||||
|
featureRecord.Name.ShouldBe("Feature1"); |
||||
|
featureRecord.GroupName.ShouldBe("Group1"); |
||||
|
featureRecord.DisplayName.ShouldBe("L:AbpFeatureManagement,Feature1"); |
||||
|
featureRecord.ValueType.ShouldBe("{\"name\":\"ToggleStringValueType\",\"properties\":{},\"validator\":{\"name\":\"BOOLEAN\",\"properties\":{}}}"); |
||||
|
featureRecord.GetProperty("CustomProperty2").ShouldBe("CustomValue2"); |
||||
|
featureRecord.AllowedProviders.ShouldBe("ProviderA,ProviderB"); |
||||
|
} |
||||
|
|
||||
|
private static FeatureGroupDefinition CreateFeatureGroup1( |
||||
|
IFeatureDefinitionContext context) |
||||
|
{ |
||||
|
var group = context.AddGroup( |
||||
|
"Group1", |
||||
|
displayName: new FixedLocalizableString("Group one") |
||||
|
); |
||||
|
|
||||
|
group["CustomProperty1"] = "CustomValue1"; |
||||
|
|
||||
|
return group; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using System; |
||||
|
using Shouldly; |
||||
|
using Volo.Abp.Validation.StringValues; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.FeatureManagement; |
||||
|
|
||||
|
public class StringValueTypeSerializer_Tests : FeatureManagementDomainTestBase |
||||
|
{ |
||||
|
private readonly StringValueTypeSerializer _serializer; |
||||
|
|
||||
|
public StringValueTypeSerializer_Tests() |
||||
|
{ |
||||
|
_serializer = GetRequiredService<StringValueTypeSerializer>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Serialize_And_Deserialize_Test() |
||||
|
{ |
||||
|
// Arrange
|
||||
|
|
||||
|
var valueType = new SelectionStringValueType |
||||
|
{ |
||||
|
ItemSource = new StaticSelectionStringValueItemSource( |
||||
|
new LocalizableSelectionStringValueItem |
||||
|
{ |
||||
|
Value = "TestValue", |
||||
|
DisplayText = new LocalizableStringInfo("TestResourceName", "TestName") |
||||
|
}), |
||||
|
Validator = new AlwaysValidValueValidator() |
||||
|
}; |
||||
|
|
||||
|
// Act
|
||||
|
|
||||
|
var valueTypeJson = _serializer.Serialize(valueType); |
||||
|
|
||||
|
//Assert
|
||||
|
valueTypeJson.ShouldBe("{\"itemSource\":{\"items\":[{\"value\":\"TestValue\",\"displayText\":{\"resourceName\":\"TestResourceName\",\"name\":\"TestName\"}}]},\"name\":\"SelectionStringValueType\",\"properties\":{},\"validator\":{\"name\":\"NULL\",\"properties\":{}}}"); |
||||
|
|
||||
|
// Act
|
||||
|
var valueType2 = _serializer.Deserialize(valueTypeJson); |
||||
|
|
||||
|
//Assert
|
||||
|
valueType2.ShouldBeOfType<SelectionStringValueType>(); |
||||
|
valueType2.Validator.ShouldBeOfType<AlwaysValidValueValidator>(); |
||||
|
valueType2.As<SelectionStringValueType>().ItemSource.Items.ShouldBeOfType<LocalizableSelectionStringValueItem[]>(); |
||||
|
valueType2.As<SelectionStringValueType>().ItemSource.Items.ShouldContain(x => |
||||
|
x.Value == "TestValue" && x.DisplayText.ResourceName == "TestResourceName" && |
||||
|
x.DisplayText.Name == "TestName"); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.PermissionManagement; |
||||
|
|
||||
|
public abstract class PermissionDefinitionRecordRepository_Tests : PermissionTestBase |
||||
|
{ |
||||
|
protected IPermissionDefinitionRecordRepository PermissionDefinitionRecordRepository { get; set; } |
||||
|
|
||||
|
protected PermissionDefinitionRecordRepository_Tests() |
||||
|
{ |
||||
|
PermissionDefinitionRecordRepository = GetRequiredService<IPermissionDefinitionRecordRepository>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task FindByNameAsync() |
||||
|
{ |
||||
|
var permission = await PermissionDefinitionRecordRepository.FindByNameAsync("MyPermission1"); |
||||
|
permission.ShouldNotBeNull(); |
||||
|
permission.Name.ShouldBe("MyPermission1"); |
||||
|
|
||||
|
permission = await PermissionDefinitionRecordRepository.FindByNameAsync("MyPermission2"); |
||||
|
permission.ShouldNotBeNull(); |
||||
|
permission.Name.ShouldBe("MyPermission2"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
namespace Volo.Abp.PermissionManagement; |
||||
|
|
||||
|
public class StaticPermissionSaver_Tests : PermissionTestBase |
||||
|
{ |
||||
|
private readonly IStaticPermissionSaver _saver; |
||||
|
|
||||
|
public StaticPermissionSaver_Tests() |
||||
|
{ |
||||
|
_saver = GetRequiredService<IStaticPermissionSaver>(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Volo.Abp.PermissionManagement.EntityFrameworkCore; |
||||
|
|
||||
|
public class EFCorePermissionDefinitionRecordRepository_Tests : PermissionDefinitionRecordRepository_Tests |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.PermissionManagement.MongoDB; |
||||
|
|
||||
|
[Collection(MongoTestCollection.Name)] |
||||
|
public class MongoDbPermissionDefinitionRecordRepository_Tests : PermissionGrantRepository_Tests<AbpPermissionManagementMongoDbTestModule> |
||||
|
{ |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue