ABP Feature
ABP Feature
2023/6/1
➡️

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);
        });
    }
}
👍🎉🎊