4.5 ABP应用层 - 功能管理

4.5.1 简介

大多数的SaaS(多租户) 应用拥有多个版本并且这些版本的功能各不相同。因此,他们能为客户提供不同的价格和功能选项。

我们可以很容易的用ABP来实现这个功能管理系统。我们能定义一些功能,检查功能是否为租户开启。这个就像ABP的设计思想(例如权限和菜单设计)。

#####关于 IFeatureValueStore 我们可以利用 IFeatureValueStore获取功能值。在module-zero项目里面已有了全部实现,当然你也能用自己的方式来扩展它们。如果没有实现该接口,NullFeatureValueStore (默认实现)会被使用,它会为所有功能值返回null(默认值将会被用在这个案例中)。

4.5.2 功能类型

系统已有两个基本的功能类型。

1. Boolean 功能

可以是true或者false。利用这个类型我们可以为租户启用(enable)或者禁用(disable)某些功能。

2. Value 功能

可以为任意值。我们可以用它来存储并者取得一个字符串,我们也可以很容易的用数字作为字符串来存储。

例如,我们有一个任务管理系统,可以限制一个月内的任务创建数量的需求。那么我们可以根据需求创建两个版本系统;一个是每个月允许创建1000个任务,而另外一个是每个月允许创建5000个任务。所以,这个功能应该用Value类型来存储,而不是用简单的Boolean类型(true或者false)。

3. 定义功能

功能应该在检查之前被定义。模块可以定义它自己的功能,通过从FeatureProvider 派生。下面代码为我们展示了一个非常简单的具有3个功能的功能提供器。

public class AppFeatureProvider : FeatureProvider
{
    public override void SetFeatures(IFeatureDefinitionContext context)
    {
        var sampleBooleanFeature = context.Create("SampleBooleanFeature", defaultValue: "false");
        sampleBooleanFeature.CreateChildFeature("SampleNumericFeature", defaultValue: "10");
        context.Create("SampleSelectionFeature", defaultValue: "B");
    }
}

在创建了功能提供器后,我们应该在我们的模块方法 PreInitialize中注册它。如下所示:

Configuration.Features.Providers.Add<AppFeatureProvider>();

4. 基本功能属性

定义功能至少需要两个属性:

  • Name:用来区分功能的唯一标识符(string类型);

  • Default value:默认值。当我们需要这个功能值的时候我们就会用到它,并且该值对当前租户是不可用的。

在这里,我们定义了一个叫做SampleBooleanFeature的boolean类型的功能,默认值是false(禁用)。 我们还定义了两个value类型的功能(SampleNumericFeature 是SampleBooleanFeature的子功能)。

建议: 创建字符串常量的功能名称并且我们可以在任何地方使用它,还可以有效避免功能名称输入错误(记忆力有时候不行咯)。

5. 其它功能属性

唯一标识(name)和默认值属性已经足够满足需求了,但这里还有一些其它的属性来满足更细粒化的控制需求。

  • Scope:枚举类型FeatureScopes的某个值 ,可以是Edition (如果该功能只对Edition 等级的用户开放),Tenant(如果该功能仅对Tenant等级的用户开放)或者All(All等级可以拥有Edition和Tenant等级的所有功能,但是租户的设置会覆盖版本的设置);默认设置是All级别。

  • DisplayName:本地化字符串,向用户展示功能名称。

  • Description:本地化字符串,向用户的详细描述该功能。

  • InputType:UI层的输入类型(控件类型:Checkbox, Combobox等)。

  • Attributes:与功能相关的自定义任意类型的数据字典。

下面代码详细展示了如何使用上面所描述的属性

public class AppFeatureProvider : FeatureProvider
{
    public override void SetFeatures(IFeatureDefinitionContext context)
    {
        var sampleBooleanFeature = context.Create(
            AppFeatures.SampleBooleanFeature,
            defaultValue: "false",
            displayName: L("Sample boolean feature"),
            inputType: new CheckboxInputType()
            );

        sampleBooleanFeature.CreateChildFeature(
            AppFeatures.SampleNumericFeature,
            defaultValue: "10",
            displayName: L("Sample numeric feature"),
            inputType: new SingleLineStringInputType(new NumericValueValidator(1, 1000000))
            );

        context.Create(
            AppFeatures.SampleSelectionFeature,
            defaultValue: "B",
            displayName: L("Sample selection feature"),
            inputType: new ComboboxInputType(
                new StaticLocalizableComboboxItemSource(
                    new LocalizableComboboxItem("A", L("Selection A")),
                    new LocalizableComboboxItem("B", L("Selection B")),
                    new LocalizableComboboxItem("C", L("Selection C"))
                    )
                )
            );
    }

    private static ILocalizableString L(string name)
    {
        return new LocalizableString(name, AbpZeroTemplateConsts.LocalizationSourceName);
    }
}

注意: 这个输入类型的定义不是被ABP使用,ABP提供这些选项让我们能够更便捷的来创建我们所需要的功能。

4.5.3 功能层次

正如上面示例所展示的,功能可以有子功能。父功能通常被定义为boolean类型的功能,如果父功能被启用,那么子功能将会是可用的。ABP不会强制这样使用,但是建议你这样使用。

4.5.4 功能检测

在系统中我们定义这些功能,是用来检测这些功能是否应该对每个用户(租户)启用(允许)或者禁用(阻止)。我们可以用不同的方法来检测它们。

2. 使用RequiresFeature特性

我们可以在类或者方法上面用RequiresFeature特性,如下所示:

[RequiresFeature("ExportToExcel")]
public async Task<FileDto> GetReportToExcel(...)
{
    ...
}

如果ExportToExcel功能为当前租户(从IAbpsession获取当前租户)启用,那么这个方法会被执行;如果没有被启用,那么将会自动的抛出一个AbpAuthorizationException 异常。

当然RequiresFeature特性应该使用的是boolean类型功能 ,否则你会得到一个异常。

  • 不能用在一个私有方法上

  • 不能用在一个静态方法上

  • 不能用在一个非注入类的方法上(我们必须使用Dependency Injection).

还有:

  • 可以用在任何public方法上,如果这个方法是通过扩展接口来实现的(就像Application Services扩展指定接口一样)

  • 方法应该是virtual方法,如果被直接从类引用中调用(就像 ASP.NET MVC 或者 Web API Controllers)。

  • 方法应该是virtual方法,如果它是一个protected.

3. 使用IFeatureChecker

我们可以注入和使用IFeatureChecker接口来手动的检测功能(它是被自动注入,并对Application Services,MVC和Web API Controllers 直接可用)。

4. IsEnabled

可以简单的检测,如果给出的功能是启用或者禁用。如下所示:

public async Task<FileDto> GetReportToExcel(...)
{
    if (await FeatureChecker.IsEnabledAsync("ExportToExcel"))
    {
        throw new AbpAuthorizationException("You don't have this feature: ExportToExcel");
    }
    
    ...
}

IsEnabledAsync 还有其他的方法也有同步版本。

当然,IsEnabled方法应该使用booean类型功能。否则,你会得到一个异常。

正如示例所示,如果你只是想检测功能并且抛出异常,你只需要使用CheckEnabled方法。

5. GetValue

获取当前功能的值并且转换为所需类型,如下所示:

var createdTaskCountInThisMonth = GetCreatedTaskCountInThisMonth();
if (createdTaskCountInThisMonth >= FeatureChecker.GetValue("MaxTaskCreationLimitPerMonth").To<int>())
{
    throw new AbpAuthorizationException("You exceed task creation limit for this month, sorry :(");
}

6. 客户端

在客户端,我们能使用abp.features命名空间去取得当前功能的值。

isEnabled

var isEnabled = abp.features.isEnabled('SampleBooleanFeature');

getValue

var value = abp.features.getValue('SampleNumericFeature');

4.5.5 功能管理

如果你需要定义一些功能,你可以注入和使用IFeatureManager。

4.5.6 版本须知

ABP没有建立版本系统,因为这样一个系统需要数据库支持(存储版本,版本功能,租客版映射等等)。因此,版本系统被实现在module zero。使用它你会很容易的拥有自己的版本系统,或者你可以自己完全的实现一个。