mirror of https://github.com/abpframework/abp.git
50 changed files with 1487 additions and 125 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,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,9 @@ |
|||
using System; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace Volo.Abp.FeatureManagement; |
|||
|
|||
public interface IFeatureDefinitionRecordRepository : IBasicRepository<FeatureDefinitionRecord, Guid> |
|||
{ |
|||
|
|||
} |
|||
@ -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,16 @@ |
|||
using System; |
|||
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) |
|||
{ |
|||
} |
|||
} |
|||
@ -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) |
|||
{ |
|||
} |
|||
} |
|||
Loading…
Reference in new issue