Feature 功能
ABP 框架中存在一个 Feature 的特性,功能和设计思路非常类似于框架中的 Authorization 功能,都 是来控制用户是否能够继续操作某项功能,不同点在于 Authorization 默认是应用在 IApplicationService 上控制用户或者其所属租户是否具有权限访问服务,而 Feature 应用更为广泛 可以控制访问任意类型,而且是无法控制具体用户的,只能是某些指定的全局范围内。注意 Feature 也是有父子关系的,只有父 Feature 可用,子 Feature 才有意义。特性的运行时值通常为 boolean 值,如 true (启用)或 false (禁用)。但是,您可以为 feature 获取/设置任何类型的值。
Feature 定义
using Volo.Abp.Features;
public class MyFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myGroup = context.AddGroup("MyApp");
myGroup.AddFeature("MyApp.PdfReporting", defaultValue: "false");
myGroup.AddFeature("MyApp.MaxProductCount", defaultValue: "10");
}
}
IFeatureDefinitionProvider 定义功能
public interface IFeatureDefinitionProvider
{
void Define(IFeatureDefinitionContext context);
}
public abstract class FeatureDefinitionProvider :
IFeatureDefinitionProvider, ITransientDependency
{
public abstract void Define(IFeatureDefinitionContext context);
}
//上下文持有的是组
public interface IFeatureDefinitionContext
{
FeatureGroupDefinition AddGroup([NotNull] string name,
ILocalizableString? displayName = null);
FeatureGroupDefinition? GetGroupOrNull(string name);
void RemoveGroup(string name);
}
FeatureGroupDefinition
组定义
public class FeatureGroupDefinition : ICanCreateChildFeature
{
public string Name { get; }
public Dictionary<string, object?> Properties { get; }
public ILocalizableString DisplayName {
get => _displayName;
set => _displayName = Check.NotNull(value, nameof(value));
}
private ILocalizableString _displayName = default!;
public IReadOnlyList<FeatureDefinition> Features => _features.ToImmutableList();
private readonly List<FeatureDefinition> _features;
public object? this[string name] {
get => Properties.GetOrDefault(name);
set => Properties[name] = value;
}
protected internal FeatureGroupDefinition(
string name,
ILocalizableString? displayName = null)
{
Name = name;
DisplayName = displayName ?? new FixedLocalizableString(Name);
Properties = new Dictionary<string, object?>();
_features = new List<FeatureDefinition>();
}
public virtual FeatureDefinition AddFeature(
string name,
string? defaultValue = null,
ILocalizableString? displayName = null,
ILocalizableString? description = null,
IStringValueType? valueType = null,
bool isVisibleToClients = true,
bool isAvailableToHost = true)
{
var feature = new FeatureDefinition(
name,
defaultValue,
displayName,
description,
valueType,
isVisibleToClients,
isAvailableToHost
);
_features.Add(feature);
return feature;
}
public FeatureDefinition CreateChildFeature(string name,
string? defaultValue = null,
ILocalizableString? displayName = null,
ILocalizableString? description = null,
IStringValueType? valueType = null,
bool isVisibleToClients = true,
bool isAvailableToHost = true)
{
return AddFeature(name, defaultValue, displayName, description,
valueType, isVisibleToClients, isAvailableToHost);
}
public virtual List<FeatureDefinition> GetFeaturesWithChildren()
{
var features = new List<FeatureDefinition>();
foreach (var feature in _features)
{
AddFeatureToListRecursively(features, feature);
}
return features;
}
public virtual FeatureGroupDefinition WithProperty(string key,
object value)
{
Properties[key] = value;
return this;
}
private void AddFeatureToListRecursively(List<FeatureDefinition>
features, FeatureDefinition feature)
{
features.Add(feature);
foreach (var child in feature.Children)
{
AddFeatureToListRecursively(features, child);
}
}
public override string ToString()
{
return $"[{nameof(FeatureGroupDefinition)} {Name}]";
}
}
FeatureDefinition
public class FeatureDefinition : ICanCreateChildFeature
{
/// <summary>
/// Unique name of the feature.
/// </summary>
[NotNull]
public string Name { get; }
[NotNull]
public ILocalizableString DisplayName {
get => _displayName;
set => _displayName = Check.NotNull(value, nameof(value));
}
private ILocalizableString _displayName = default!;
public ILocalizableString? Description { get; set; }
/// <summary>
/// Parent of this feature, if one exists.
/// If set, this feature can be enabled only if the parent is enabled.
/// </summary>
public FeatureDefinition? Parent { get; private set; }
/// <summary>
/// List of child features.
/// </summary>
public IReadOnlyList<FeatureDefinition> Children => _children.ToImmutableList();
private readonly List<FeatureDefinition> _children;
/// <summary>
/// Default value of the feature.
/// </summary>
public string? DefaultValue { get; set; }
/// <summary>
/// Can clients see this feature and it's value.
/// Default: true.
/// </summary>
public bool IsVisibleToClients { get; set; }
/// <summary>
/// Can host use this feature.
/// Default: true.
/// </summary>
public bool IsAvailableToHost { get; set; }
/// <summary>
/// A list of allowed providers to get/set value of this feature.
/// An empty list indicates that all providers are allowed.
/// </summary>
[NotNull]
public List<string> AllowedProviders { get; }
/// <summary>
/// Gets/sets a key-value on the <see cref="Properties"/>.
/// </summary>
/// <param name="name">Name of the property</param>
/// <returns>
/// Returns the value in the <see cref="Properties"/>
/// dictionary by given <paramref name="name"/>.
/// Returns null if given <paramref name="name"/>
is not present in the <see cref="Properties"/> dictionary.
/// </returns>
public object? this[string name] {
get => Properties.GetOrDefault(name);
set => Properties[name] = value;
}
/// <summary>
/// Can be used to get/set custom properties for this feature.
/// </summary>
[NotNull]
public Dictionary<string, object?> Properties { get; }
/// <summary>
/// Input type.
/// This can be used to prepare an input for changing this feature's value.
/// Default: <see cref="ToggleStringValueType"/>.
/// </summary>
public IStringValueType? ValueType { get; set; }
public FeatureDefinition(
string name,
string? defaultValue = null,
ILocalizableString? displayName = null,
ILocalizableString? description = null,
IStringValueType? valueType = null,
bool isVisibleToClients = true,
bool isAvailableToHost = true)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
DefaultValue = defaultValue;
DisplayName = displayName ?? new FixedLocalizableString(name);
Description = description;
ValueType = valueType ?? new ToggleStringValueType();
IsVisibleToClients = isVisibleToClients;
IsAvailableToHost = isAvailableToHost;
Properties = new Dictionary<string, object?>();
AllowedProviders = new List<string>();
_children = new List<FeatureDefinition>();
}
/// <summary>
/// Sets a property in the <see cref="Properties"/> dictionary.
/// This is a shortcut for nested calls on this object.
/// </summary>
public virtual FeatureDefinition WithProperty(string key, object value)
{
Properties[key] = value;
return this;
}
/// <summary>
/// Adds one or more providers to the <see cref="AllowedProviders"/> list.
/// This is a shortcut for nested calls on this object.
/// </summary>
public virtual FeatureDefinition WithProviders(params string[] providers)
{
if (!providers.IsNullOrEmpty())
{
AllowedProviders.AddRange(providers);
}
return this;
}
/// <summary>
/// Adds a child feature.
/// </summary>
/// <returns>Returns a newly created child feature</returns>
public FeatureDefinition CreateChild(
string name,
string? defaultValue = null,
ILocalizableString? displayName = null,
ILocalizableString? description = null,
IStringValueType? valueType = null,
bool isVisibleToClients = true,
bool isAvailableToHost = true)
{
var feature = new FeatureDefinition(
name,
defaultValue,
displayName,
description,
valueType,
isVisibleToClients,
isAvailableToHost)
{
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 FeatureDefinition CreateChildFeature(string name,
string? defaultValue = null,
ILocalizableString? displayName = null,
ILocalizableString? description = null,
IStringValueType? valueType = null,
bool isVisibleToClients = true,
bool isAvailableToHost = true)
{
return this.CreateChild(name, defaultValue, displayName, description,
valueType, isVisibleToClients, isAvailableToHost);
}
public override string ToString()
{
return $"[{nameof(FeatureDefinition)}: {Name}]";
}
}
IFeatureValueProvider
public interface IFeatureValueProvider
{
string Name { get; }
Task<string?> GetOrNullAsync([NotNull] FeatureDefinition feature);
}
FeatureValueProvider
public abstract class FeatureValueProvider : IFeatureValueProvider, ITransientDependency
{
public abstract string Name { get; }
protected IFeatureStore FeatureStore { get; }
protected FeatureValueProvider(IFeatureStore featureStore)
{
FeatureStore = featureStore;
}
public abstract Task<string?> GetOrNullAsync(FeatureDefinition feature);
}
DefaultValueFeatureValueProvider
public class DefaultValueFeatureValueProvider : FeatureValueProvider
{
public const string ProviderName = "D";
public override string Name => ProviderName;
public DefaultValueFeatureValueProvider(IFeatureStore settingStore)
: base(settingStore)
{
}
public override Task<string?> GetOrNullAsync(FeatureDefinition setting)
{
return Task.FromResult<string?>(setting.DefaultValue);
}
}
EditionFeatureValueProvider 用户可编辑的 提供 stroe 进行持久化
public class EditionFeatureValueProvider : FeatureValueProvider
{
public const string ProviderName = "E";
public override string Name => ProviderName;
protected ICurrentPrincipalAccessor PrincipalAccessor;
public EditionFeatureValueProvider(IFeatureStore featureStore,
ICurrentPrincipalAccessor principalAccessor)
: base(featureStore)
{
PrincipalAccessor = principalAccessor;
}
public override async Task<string?> GetOrNullAsync(FeatureDefinition feature)
{
var editionId = PrincipalAccessor.Principal?.FindEditionId();
if (editionId == null)
{
return null;
}
return await FeatureStore.GetOrNullAsync(feature.Name, Name,
editionId.Value.ToString());
}
}
TenantFeatureValueProvider 租户级别的
public class TenantFeatureValueProvider : FeatureValueProvider
{
public const string ProviderName = "T";
public override string Name => ProviderName;
protected ICurrentTenant CurrentTenant { get; }
public TenantFeatureValueProvider(IFeatureStore featureStore,
ICurrentTenant currentTenant)
: base(featureStore)
{
CurrentTenant = currentTenant;
}
public override async Task<string?> GetOrNullAsync(FeatureDefinition feature)
{
return await FeatureStore.GetOrNullAsync(feature.Name, Name,
CurrentTenant.Id?.ToString());
}
}
AbpFeatureOptions
相关的设置保存在选项 AbpFeatureOptions 单例,提供静态的 IFeatureDefinitionProvider
public class AbpFeatureOptions
{
public ITypeList<IFeatureDefinitionProvider> DefinitionProviders { get; }
public ITypeList<IFeatureValueProvider> ValueProviders { get; }
public HashSet<string> DeletedFeatures { get; }
public HashSet<string> DeletedFeatureGroups { get; }
public AbpFeatureOptions()
{
DefinitionProviders = new TypeList<IFeatureDefinitionProvider>();
ValueProviders = new TypeList<IFeatureValueProvider>();
DeletedFeatures = new HashSet<string>();
DeletedFeatureGroups = new HashSet<string>();
}
}
IFeatureDefinitionManager 获取所有的动态和静态 FeatureDefinition
public interface IFeatureDefinitionManager
{
[NotNull]
Task<FeatureDefinition> GetAsync([NotNull] string name);
Task<IReadOnlyList<FeatureDefinition>> GetAllAsync();
Task<FeatureDefinition?> GetOrNullAsync(string name);
Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync();
}
public class FeatureDefinitionManager : IFeatureDefinitionManager, ISingletonDependency
{
protected IStaticFeatureDefinitionStore StaticStore;
protected IDynamicFeatureDefinitionStore DynamicStore;
public FeatureDefinitionManager(
IStaticFeatureDefinitionStore staticStore,
IDynamicFeatureDefinitionStore dynamicStore)
{
StaticStore = staticStore;
DynamicStore = dynamicStore;
}
public virtual async Task<FeatureDefinition> GetAsync(string name)
{
var permission = await GetOrNullAsync(name);
if (permission == null)
{
throw new AbpException("Undefined feature: " + name);
}
return permission;
}
public virtual async Task<FeatureDefinition?> GetOrNullAsync(string name)
{
Check.NotNull(name, nameof(name));
return await StaticStore.GetOrNullAsync(name) ??
await DynamicStore.GetOrNullAsync(name);
}
public virtual async Task<IReadOnlyList<FeatureDefinition>> GetAllAsync()
{
var staticFeatures = await StaticStore.GetFeaturesAsync();
var staticFeatureNames = staticFeatures
.Select(p => p.Name)
.ToImmutableHashSet();
var dynamicFeatures = await DynamicStore.GetFeaturesAsync();
/* We prefer static features over dynamics */
return staticFeatures.Concat(
dynamicFeatures.Where(d => !staticFeatureNames.Contains(d.Name))
).ToImmutableList();
}
public virtual async Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync()
{
var staticGroups = await StaticStore.GetGroupsAsync();
var staticGroupNames = staticGroups
.Select(p => p.Name)
.ToImmutableHashSet();
var dynamicGroups = await DynamicStore.GetGroupsAsync();
/* We prefer static groups over dynamics */
return staticGroups.Concat(
dynamicGroups.Where(d => !staticGroupNames.Contains(d.Name))
).ToImmutableList();
}
}
IFeatureChecker 提供给用户的 api 用来做自定义的功能检查
namespace Volo.Abp.Features;
public interface IFeatureChecker
{
//值检查器
Task<string?> GetOrNullAsync([NotNull] string name);
// bool 检查器
Task<bool> IsEnabledAsync(string name);
}
//
public abstract class FeatureCheckerBase : IFeatureChecker, ITransientDependency
{
public abstract Task<string?> GetOrNullAsync(string name);
// bool 检查
public virtual async Task<bool> IsEnabledAsync(string name)
{
var value = await GetOrNullAsync(name);
if (value.IsNullOrEmpty())
{
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
);
}
}
}
FeatureChecker
public class FeatureChecker : FeatureCheckerBase
{
protected AbpFeatureOptions Options { get; }
protected IServiceProvider ServiceProvider { get; }
protected IFeatureDefinitionManager FeatureDefinitionManager { get; }
protected List<IFeatureValueProvider> Providers => _providers.Value;
private readonly Lazy<List<IFeatureValueProvider>> _providers;
public FeatureChecker(
//提供静态的 FeatureDefinition 及 值提供程序
IOptions<AbpFeatureOptions> options,
IServiceProvider serviceProvider,
//获取动态的 FeatureDefinition 及静态的 FeatureDefinition
IFeatureDefinitionManager featureDefinitionManager)
{
ServiceProvider = serviceProvider;
FeatureDefinitionManager = featureDefinitionManager;
Options = options.Value;
_providers = new Lazy<List<IFeatureValueProvider>>(
() => Options
.ValueProviders
.Select(type => (ServiceProvider.GetRequiredService(type)
as IFeatureValueProvider)!)
.ToList(),
true
);
}
public override async Task<string?> GetOrNullAsync(string name)
{
var featureDefinition = await FeatureDefinitionManager.GetAsync(name);
var providers = Enumerable
.Reverse(Providers);
if (featureDefinition.AllowedProviders.Any())
{
providers = providers.Where(p => featureDefinition.AllowedProviders.
Contains(p.Name));
}
return await GetOrNullValueFromProvidersAsync(providers, featureDefinition);
}
protected virtual async Task<string?> GetOrNullValueFromProvidersAsync(
IEnumerable<IFeatureValueProvider> providers,
FeatureDefinition feature)
{
foreach (var provider in providers)
{
var value = await provider.GetOrNullAsync(feature);
if (value != null)
{
return value;
}
}
return null;
}
}
FakeFeatureChecker
public class FakeFeatureChecker : FeatureCheckerBase
{
public override Task<string> GetOrNullAsync(string name)
{
return Task.FromResult(GetOrNull(name));
}
private static string GetOrNull(string name)
{
switch (name)
{
case "AllowedFeature":
return true.ToString();
case "NotAllowedFeature":
return null; //or false, doesn't matter
}
throw new ApplicationException($"Unknown feature: '{name}'");
}
}
FeatureCheckerExtensions 扩展方法更方便使用
namespace Volo.Abp.Features;
public static class FeatureCheckerExtensions
{
public static async Task<T> GetAsync<T>(
[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<T>() ?? defaultValue;
}
public static async Task<bool> IsEnabledAsync(this IFeatureChecker
featureChecker, bool requiresAll, params string[] featureNames)
{
if (featureNames.IsNullOrEmpty())
{
return true;
}
if (requiresAll)
{
foreach (var featureName in featureNames)
{
if (!(await featureChecker.IsEnabledAsync(featureName)))
{
return false;
}
}
return true;
}
foreach (var featureName in featureNames)
{
if (await featureChecker.IsEnabledAsync(featureName))
{
return true;
}
}
return false;
}
public static async Task CheckEnabledAsync(
this IFeatureChecker featureChecker, string featureName)
{
if (!(await featureChecker.IsEnabledAsync(featureName)))
{
throw new AbpAuthorizationException(
code: AbpFeatureErrorCodes.FeatureIsNotEnabled).WithData(
"FeatureName", featureName);
}
}
public static async Task CheckEnabledAsync(
this IFeatureChecker featureChecker, bool requiresAll, params string[] featureNames)
{
if (featureNames.IsNullOrEmpty())
{
return;
}
if (requiresAll)
{
foreach (var featureName in featureNames)
{
if (!(await featureChecker.IsEnabledAsync(featureName)))
{
throw new AbpAuthorizationException(
code: AbpFeatureErrorCodes.AllOfTheseFeaturesMustBeEnabled)
.WithData("FeatureNames", string.Join(", ", featureNames));
}
}
}
else
{
foreach (var featureName in featureNames)
{
if (await featureChecker.IsEnabledAsync(featureName))
{
return;
}
}
throw new AbpAuthorizationException(
code: AbpFeatureErrorCodes.AtLeastOneOfTheseFeaturesMustBeEnabled)
.WithData("FeatureNames", string.Join(", ", featureNames));
}
}
}
RequiresFeatureAttribute 增加此 Attribute,可自动验证功能,只能验证 bool
需要验证的 feature name
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequiresFeatureAttribute : Attribute
{
public string[] Features { get; }
public bool RequiresAll { get; set; }
public RequiresFeatureAttribute(params string[] features)
{
Features = features ?? Array.Empty<string>();
}
}
禁用 DisableFeatureCheck
[AttributeUsage(AttributeTargets.Method)]
public class DisableFeatureCheckAttribute : Attribute
{
}
FeatureInterceptor 验证定义的 RequiresFeatureAttribute 的类或方法
namespace Volo.Abp.Features;
public class FeatureInterceptor : AbpInterceptor, ITransientDependency
{
private readonly IServiceScopeFactory _serviceScopeFactory;
//顶级 IServiceScopeFactory
public FeatureInterceptor(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{
//是否已经检查过此功能了,检查过就不检查了
if (AbpCrossCuttingConcerns.IsApplied(
invocation.TargetObject, AbpCrossCuttingConcerns.FeatureChecking))
{
await invocation.ProceedAsync();
return;
}
//应用功能检查
await CheckFeaturesAsync(invocation);
await invocation.ProceedAsync();
}
protected virtual async Task CheckFeaturesAsync(
IAbpMethodInvocation invocation)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<
IMethodInvocationFeatureCheckerService>().CheckAsync(
new MethodInvocationFeatureCheckerContext(
invocation.Method
)
);
}
}
}
MethodInvocationFeatureCheckerService 检查服务
public class MethodInvocationFeatureCheckerService :
IMethodInvocationFeatureCheckerService, ITransientDependency
{
private readonly IFeatureChecker _featureChecker;
public MethodInvocationFeatureCheckerService(
IFeatureChecker featureChecker)
{
_featureChecker = featureChecker;
}
public async Task CheckAsync(MethodInvocationFeatureCheckerContext context)
{
if (IsFeatureCheckDisabled(context))
{
return;
}
foreach (var requiresFeatureAttribute
in GetRequiredFeatureAttributes(context.Method))
{
await _featureChecker.CheckEnabledAsync(
requiresFeatureAttribute.RequiresAll, requiresFeatureAttribute.Features);
}
}
protected virtual bool IsFeatureCheckDisabled(
MethodInvocationFeatureCheckerContext context)
{
return context.Method
.GetCustomAttributes(true)
.OfType<DisableFeatureCheckAttribute>()
.Any();
}
protected virtual IEnumerable<RequiresFeatureAttribute>
GetRequiredFeatureAttributes(MethodInfo methodInfo)
{
var attributes = methodInfo
.GetCustomAttributes(true)
.OfType<RequiresFeatureAttribute>();
if (methodInfo.IsPublic)
{
attributes = attributes
.Union(
methodInfo.DeclaringType!
.GetCustomAttributes(true)
.OfType<RequiresFeatureAttribute>()
);
}
return attributes;
}
}
AbpFeaturesModule 功能管理模块
public class AbpFeaturesModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
//增加功能管理拦截器
context.Services.OnRegistered(FeatureInterceptorRegistrar.RegisterIfNeeded);
//添加所有的定义提供器给 AbpFeatureOptions
AutoAddDefinitionProviders(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
//增加值提供器
context.Services.Configure<AbpFeatureOptions>(options =>
{
options.ValueProviders.Add<DefaultValueFeatureValueProvider>();
options.ValueProviders.Add<EditionFeatureValueProvider>();
options.ValueProviders.Add<TenantFeatureValueProvider>();
});
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<AbpFeatureResource>();
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Add<AbpFeatureResource>("en")
.AddVirtualJson("/Volo/Abp/Features/Localization");
});
Configure<AbpExceptionLocalizationOptions>(options =>
{
options.MapCodeNamespace("Volo.Feature", typeof(AbpFeatureResource));
});
}
private static void AutoAddDefinitionProviders(IServiceCollection services)
{
var definitionProviders = new List<Type>();
services.OnRegistered(context =>
{
if (typeof(IFeatureDefinitionProvider).IsAssignableFrom(
context.ImplementationType))
{
definitionProviders.Add(context.ImplementationType);
}
});
services.Configure<AbpFeatureOptions>(options =>
{
options.DefinitionProviders.AddIfNotContains(definitionProviders);
});
}
}
👍🎉🎊