diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/NameValue.cs b/framework/src/Volo.Abp.Core/Volo/Abp/NameValue.cs index a9525b9064..c7a4e7c0fe 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/NameValue.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/NameValue.cs @@ -8,17 +8,11 @@ namespace Volo.Abp [Serializable] public class NameValue : NameValue { - /// - /// Creates a new . - /// public NameValue() { } - /// - /// Creates a new . - /// public NameValue(string name, string value) { Name = name; @@ -42,17 +36,11 @@ namespace Volo.Abp /// public T Value { get; set; } - /// - /// Creates a new . - /// public NameValue() { } - /// - /// Creates a new . - /// public NameValue(string name, T value) { Name = name; diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs new file mode 100644 index 0000000000..17bd05db6d --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureChecker.cs @@ -0,0 +1,86 @@ +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 FeatureChecker : IFeatureChecker, ITransientDependency + { + protected IFeatureDefinitionManager FeatureDefinitionManager { get; } + protected Lazy> Providers { get; } + protected FeatureOptions Options { get; } + + public FeatureChecker( + IOptions options, + IServiceProvider serviceProvider, + IFeatureDefinitionManager featureDefinitionManager) + { + FeatureDefinitionManager = featureDefinitionManager; + + Options = options.Value; + + Providers = new Lazy>( + () => Options + .ValueProviders + .Select(type => serviceProvider.GetRequiredService(type) as IFeatureValueProvider) + .ToList(), + true + ); + } + + public virtual async Task GetOrNullAsync(string name) + { + var featureDefinition = FeatureDefinitionManager.Get(name); + var providers = Enumerable + .Reverse(Providers.Value); + + if (featureDefinition.AllowedProviders.Any()) + { + providers = providers.Where(p => featureDefinition.AllowedProviders.Contains(p.Name)); + } + + return await GetOrNullValueFromProvidersAsync(providers, featureDefinition); + } + + public async Task IsEnabledAsync(string name) + { + var value = await GetOrNullAsync(name); + if (value == null) + { + return false; + } + + try + { + return bool.Parse(value); + } + catch (Exception ex) + { + throw new AbpException( + $"The value '{value}' for the feature '{name}' should be a boolean, but was not!", + ex + ); + } + } + + protected virtual async Task GetOrNullValueFromProvidersAsync( + IEnumerable providers, + FeatureDefinition feature) + { + foreach (var provider in providers) + { + var value = await provider.GetOrNullAsync(feature); + if (value != null) + { + return value; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs new file mode 100644 index 0000000000..05122e8e6a --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureCheckerExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Threading; + +namespace Volo.Abp.Features +{ + public static class FeatureCheckerExtensions + { + public static async Task GetAsync([NotNull] this IFeatureChecker featureChecker, [NotNull] string name, T defaultValue = default) + where T : struct + { + Check.NotNull(featureChecker, nameof(featureChecker)); + Check.NotNull(name, nameof(name)); + + var value = await featureChecker.GetOrNullAsync(name); + return value?.To() ?? defaultValue; + } + + public static string GetOrNull([NotNull] this IFeatureChecker featureChecker, [NotNull] string name) + { + Check.NotNull(featureChecker, nameof(featureChecker)); + return AsyncHelper.RunSync(() => featureChecker.GetOrNullAsync(name)); + } + + public static T Get([NotNull] this IFeatureChecker featureChecker, [NotNull] string name, T defaultValue = default) + where T : struct + { + return AsyncHelper.RunSync(() => featureChecker.GetAsync(name, defaultValue)); + } + + public static bool IsEnabled([NotNull] this IFeatureChecker featureChecker, [NotNull] string name) + { + return AsyncHelper.RunSync(() => featureChecker.IsEnabledAsync(name)); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs new file mode 100644 index 0000000000..675697334f --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using JetBrains.Annotations; +using Volo.Abp.Localization; + +namespace Volo.Abp.Features +{ + public class FeatureDefinition + { + /// + /// Unique name of the feature. + /// + [NotNull] + public string Name { get; } + + [NotNull] + public ILocalizableString DisplayName + { + get => _displayName; + set => _displayName = Check.NotNull(value, nameof(value)); + } + private ILocalizableString _displayName; + + [CanBeNull] + public ILocalizableString Description { get; set; } + + /// + /// Parent of this feature, if one exists. + /// If set, this feature can be enabled only if the parent is enabled. + /// + public FeatureDefinition Parent { get; private set; } + + /// + /// List of child features. + /// + public IReadOnlyList Children => _children.ToImmutableList(); + private readonly List _children; + + /// + /// Default value of the feature. + /// + [CanBeNull] + public string DefaultValue { get; set; } + + /// + /// Can clients see this feature and it's value. + /// Default: true. + /// + public bool IsVisibleToClients { get; set; } + + /// + /// A list of allowed providers to get/set value of this feature. + /// An empty list indicates that all providers are allowed. + /// + public List AllowedProviders { get; } + + /// + /// Can be used to get/set custom properties for this feature. + /// + [NotNull] + public Dictionary Properties { get; } + + //TODO: Implement input type like old ABP! + + public FeatureDefinition( + string name, + string defaultValue = null, + ILocalizableString displayName = null, + ILocalizableString description = null, + bool isVisibleToClients = true) + { + Name = name; + DefaultValue = defaultValue; + IsVisibleToClients = isVisibleToClients; + DisplayName = displayName ?? new FixedLocalizableString(name); + Description = description; + + Properties = new Dictionary(); + AllowedProviders = new List(); + _children = new List(); + } + + /// + /// Sets a property in the dictionary. + /// This is a shortcut for nested calls on this object. + /// + public virtual FeatureDefinition WithProperty(string key, object value) + { + Properties[key] = value; + return this; + } + + /// + /// Sets a property in the dictionary. + /// This is a shortcut for nested calls on this object. + /// + public virtual FeatureDefinition WithProviders(params string[] providers) + { + if (!providers.IsNullOrEmpty()) + { + AllowedProviders.AddRange(providers); + } + + return this; + } + + /// + /// Adds a child feature. + /// + /// Returns a newly created child feature + public FeatureDefinition CreateChild( + string name, + string defaultValue = null, + ILocalizableString displayName = null, + ILocalizableString description = null, + bool isVisibleToClients = true) + { + var feature = new FeatureDefinition( + name, + defaultValue, + displayName, + description, + isVisibleToClients) + { + Parent = this + }; + + _children.Add(feature); + return feature; + } + + public void RemoveChild(string name) + { + var featureToRemove = _children.FirstOrDefault(f => f.Name == name); + if (featureToRemove == null) + { + throw new AbpException($"Could not find a feature named '{name}' in the Children of this feature '{Name}'."); + } + + featureToRemove.Parent = null; + _children.Remove(featureToRemove); + } + + public override string ToString() + { + return $"[{nameof(FeatureDefinition)}: {Name}]"; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionContext.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionContext.cs new file mode 100644 index 0000000000..d14695b30a --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionContext.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Volo.Abp.Features +{ + public class FeatureDefinitionContext : IFeatureDefinitionContext + { + protected Dictionary Features { get; } + + public FeatureDefinitionContext(Dictionary features) + { + Features = features; + } + + public virtual FeatureDefinition GetOrNull(string name) + { + return Features.GetOrDefault(name); + } + + public virtual IReadOnlyList GetAll() + { + return Features.Values.ToImmutableList(); + } + + public virtual void Add(params FeatureDefinition[] definitions) + { + if (definitions.IsNullOrEmpty()) + { + return; + } + + foreach (var definition in definitions) + { + Features[definition.Name] = definition; + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs new file mode 100644 index 0000000000..bf36c114d3 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionManager.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features +{ + public class FeatureDefinitionManager : IFeatureDefinitionManager, ISingletonDependency + { + protected Lazy> Providers { get; } + + protected Lazy> FeatureDefinitions { get; } + + protected FeatureOptions Options { get; } + + private readonly IServiceProvider _serviceProvider; + + public FeatureDefinitionManager( + IOptions options, + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + Options = options.Value; + + Providers = new Lazy>(CreateFeatureProviders, true); + FeatureDefinitions = new Lazy>(CreateFeatureDefinitions, true); + } + + public virtual FeatureDefinition Get(string name) + { + Check.NotNull(name, nameof(name)); + + var feature = GetOrNull(name); + + if (feature == null) + { + throw new AbpException("Undefined feature: " + name); + } + + return feature; + } + + public virtual IReadOnlyList GetAll() + { + return FeatureDefinitions.Value.Values.ToImmutableList(); + } + + public virtual FeatureDefinition GetOrNull(string name) + { + return FeatureDefinitions.Value.GetOrDefault(name); + } + + protected virtual List CreateFeatureProviders() + { + return Options + .DefinitionProviders + .Select(p => _serviceProvider.GetRequiredService(p) as IFeatureDefinitionProvider) + .ToList(); + } + + protected virtual IDictionary CreateFeatureDefinitions() + { + var features = new Dictionary(); + + foreach (var provider in Providers.Value) + { + provider.Define(new FeatureDefinitionContext(features)); + } + + return features; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionProvider.cs new file mode 100644 index 0000000000..f3c7a7f9b0 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionProvider.cs @@ -0,0 +1,9 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features +{ + public abstract class FeatureDefinitionProvider : IFeatureDefinitionProvider, ISingletonDependency + { + public abstract void Define(IFeatureDefinitionContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureOptions.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureOptions.cs new file mode 100644 index 0000000000..f988acf34c --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureOptions.cs @@ -0,0 +1,17 @@ +using Volo.Abp.Collections; + +namespace Volo.Abp.Features +{ + public class FeatureOptions + { + public ITypeList DefinitionProviders { get; } + + public ITypeList ValueProviders { get; } + + public FeatureOptions() + { + DefinitionProviders = new TypeList(); + ValueProviders = new TypeList(); + } + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureValue.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureValue.cs new file mode 100644 index 0000000000..22b7b6ab89 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureValue.cs @@ -0,0 +1,19 @@ +using System; + +namespace Volo.Abp.Features +{ + [Serializable] + public class FeatureValue : NameValue + { + public FeatureValue() + { + + } + + public FeatureValue(string name, string value) + { + Name = name; + Value = value; + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureValueProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureValueProvider.cs new file mode 100644 index 0000000000..b93b6903e2 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureValueProvider.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features +{ + public abstract class FeatureValueProvider : IFeatureValueProvider, ISingletonDependency + { + public abstract string Name { get; } + + protected IFeatureStore FeatureStore { get; } + + protected FeatureValueProvider(IFeatureStore featureStore) + { + FeatureStore = featureStore; + } + + public abstract Task GetOrNullAsync(FeatureDefinition feature); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs new file mode 100644 index 0000000000..2676506733 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureChecker.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using System.Threading.Tasks; + +namespace Volo.Abp.Features +{ + public interface IFeatureChecker + { + Task GetOrNullAsync([NotNull] string name); + + Task IsEnabledAsync(string name); + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionContext.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionContext.cs new file mode 100644 index 0000000000..8762b9d34b --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionContext.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Features +{ + public interface IFeatureDefinitionContext + { + FeatureDefinition GetOrNull(string name); + + void Add(params FeatureDefinition[] definitions); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs new file mode 100644 index 0000000000..c0172d7bc3 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionManager.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.Features +{ + public interface IFeatureDefinitionManager + { + [NotNull] + FeatureDefinition Get([NotNull] string name); + + IReadOnlyList GetAll(); + + FeatureDefinition GetOrNull(string name); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionProvider.cs new file mode 100644 index 0000000000..30e2f6b9f7 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureDefinitionProvider.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.Features +{ + public interface IFeatureDefinitionProvider + { + void Define(IFeatureDefinitionContext context); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureStore.cs new file mode 100644 index 0000000000..58cce989c7 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureStore.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.Features +{ + public interface IFeatureStore + { + Task GetOrNullAsync( + [NotNull] string name, + [CanBeNull] string providerName, + [CanBeNull] string providerKey + ); + } +} diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureValueProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureValueProvider.cs new file mode 100644 index 0000000000..67eee5228e --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/IFeatureValueProvider.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Volo.Abp.Features +{ + public interface IFeatureValueProvider + { + string Name { get; } + + Task GetOrNullAsync([NotNull] FeatureDefinition feature); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/NullFeatureStore.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/NullFeatureStore.cs new file mode 100644 index 0000000000..2a4fcee0c6 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/NullFeatureStore.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Features +{ + public class NullFeatureStore : IFeatureStore, ISingletonDependency + { + public ILogger Logger { get; set; } + + public NullFeatureStore() + { + Logger = NullLogger.Instance; + } + + public Task GetOrNullAsync(string name, string providerName, string providerKey) + { + return Task.FromResult((string) null); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Features/Volo/Abp/Features/TenantFeatureValueProvider.cs b/framework/src/Volo.Abp.Features/Volo/Abp/Features/TenantFeatureValueProvider.cs new file mode 100644 index 0000000000..73b07b6e88 --- /dev/null +++ b/framework/src/Volo.Abp.Features/Volo/Abp/Features/TenantFeatureValueProvider.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Features +{ + public class TenantFeatureValueProvider : FeatureValueProvider + { + public const string ProviderName = "Tenant"; + + public override string Name => ProviderName; + + protected ICurrentTenant CurrentTenant { get; } + + public TenantFeatureValueProvider(IFeatureStore featureStore, ICurrentTenant currentTenant) + : base(featureStore) + { + CurrentTenant = currentTenant; + } + + public override async Task GetOrNullAsync(FeatureDefinition feature) + { + return await FeatureStore.GetOrNullAsync(feature.Name, Name, CurrentTenant.Id?.ToString()); + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs index 63abadae12..3629c9eadb 100644 --- a/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs +++ b/framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingProvider.cs @@ -29,7 +29,7 @@ namespace Volo.Abp.Settings Providers = new Lazy>( () => Options .ValueProviders - .Select(c => serviceProvider.GetRequiredService(c) as ISettingValueProvider) + .Select(type => serviceProvider.GetRequiredService(type) as ISettingValueProvider) .ToList(), true );