功能管理:基于 ABP 的 Feature Management 实现动态开关
🚀 功能管理:基于 ABP 的 Feature Management 实现动态开关
📚 目录
- 🚀 功能管理:基于 ABP 的 Feature Management 实现动态开关
- 📚 一、背景分析
- 🧩 二、核心功能设计
- 2.1 定义 Feature 常量与分组
- 2.2 实现 FeatureDefinitionProvider 🛠️
- 2.3 注册到模块 ⚙️
- 2.3.1 ABP 特性注册流程图
- 2.4 使用 [RequiresFeature] 控制访问 🔒
- 2.5 后台 UI 支持 🖥️
- 2.5.1 React 前端路由示例
- 2.5.2 Angular 前端路由示例
- 🔍 三、实战示例
- 3.1 📘 场景一:PDF 报表开关
- 3.2 📕 场景二:导出限额控制
- 🔧 四、扩展内容
- 4.1 🌐 本地化资源支持
- 4.2 🖥️ UI 模块接入说明
- 4.3 🎨 灰度流程图
- 4.4 🧪 单元测试
📚 一、背景分析
在 SaaS 场景中,业务常常要求:
- 🌐 不同租户具备不同功能开关
- 🔀 根据套餐或版本灰度发布新功能
- 🎯 控制功能粒度细化、灵活
ABP 的 Feature Management 模块内建对租户、主机、版本多级别支持,配合后台 UI 可视化管理界面,让开发者无需侵入业务逻辑即可控制功能可用性和灰度发布。
🧩 二、核心功能设计
2.1 定义 Feature 常量与分组
// 文件:MyApp.Domain.Shared/Feature/MyAppFeatures.cs
namespace MyApp.FeatureManagement
{/// <summary>/// 功能标识常量/// </summary>public static class MyAppFeatures{// 布尔型开关:控制是否启用 PDF 报表功能public const string EnablePdfReport = "MyApp.EnablePdfReport";// 数值型限额:导出功能限额public const string ExportLimit = "MyApp.ExportLimit";}
}
2.2 实现 FeatureDefinitionProvider 🛠️
将以下类放在 MyApp.Domain.Shared
或其他 ABP 扫描范围内的项目中,框架会自动发现并加载。
// 文件:MyApp.Domain.Shared/Feature/MyAppFeatureDefinitionProvider.cs
using Volo.Abp.FeatureManagement;
using Volo.Abp.FeatureManagement.Definitions;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;namespace MyApp.FeatureManagement
{/// <summary>/// 定义 MyApp 相关的 Feature/// </summary>public class MyAppFeatureDefinitionProvider : FeatureDefinitionProvider{public override void Define(IFeatureDefinitionContext context){// 将所有 MyApp 下的 Feature 放到同一分组var group = context.AddGroup("MyApp",LocalizableString.Create<MyAppResource>("MyApp"));// 布尔型开关,仅允许 true/falsegroup.AddFeature(MyAppFeatures.EnablePdfReport,defaultValue: "false",displayName: LocalizableString.Create<MyAppResource>("EnablePdfReport"),valueType: new ToggleStringValueType());// 自由文本型,通过 NumericValueValidator 限制数值范围 1~10000group.AddFeature(MyAppFeatures.ExportLimit,defaultValue: "100",displayName: LocalizableString.Create<MyAppResource>("ExportLimit"),valueType: new FreeTextStringValueType(new NumericValueValidator(1, 10000)));}}
}
ℹ️ 说明:
- 在 ABP v9.1.3 中,只要将
FeatureDefinitionProvider
放在被框架扫描的项目(如Domain.Shared
),就会自动注册。- 若需要集中管理加载顺序,可在应用模块中显式通过
AbpFeatureManagementOptions.DefinitionProviders.Add<>()
注册。
2.3 注册到模块 ⚙️
如果您希望显式手动注册 FeatureDefinitionProvider
,可在模块中添加以下配置;否则框架会自动扫描加载,无需再写这段。
// 文件:MyApp.Application/MyAppApplicationModule.cs
using Volo.Abp.Modules;
using Volo.Abp.FeatureManagement;namespace MyApp
{[DependsOn(typeof(MyAppDomainSharedModule),typeof(AbpFeatureManagementDomainModule))]public class MyAppApplicationModule : AbpModule{public override void ConfigureServices(ServiceConfigurationContext context){Configure<AbpFeatureManagementOptions>(options =>{// 可选:显式添加定义提供者// options.DefinitionProviders.Add<MyAppFeatureDefinitionProvider>();});}}
}
2.3.1 ABP 特性注册流程图
2.4 使用 [RequiresFeature] 控制访问 🔒
以下示例演示如何在 Controller 上使用 [RequiresFeature]
,仅当租户启用对应功能时才允许访问;否则返回 HTTP 403。
// 文件:MyApp.Web/Controllers/ReportController.cs
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Features;namespace MyApp.Web.Controllers
{[Route("api/report")]public class ReportController : AbpController{// 仅当租户启用 EnablePdfReport 时,才能访问此接口;否则返回 403[RequiresFeature(MyAppFeatures.EnablePdfReport)][HttpGet("pdf")]public async Task<IActionResult> GeneratePdfReportAsync(){// 报表生成逻辑await Task.CompletedTask;return Ok("📄 PDF 报表已生成");}}
}
📝 注:
[RequiresFeature]
仅在被依赖注入容器管理的 Controller 或 ApplicationService 上生效;若在普通类或非 DI 管理的类方法上使用,则不会触发拦截 citeturn1search2。- 被拦截后会返回 HTTP 403,且消息中会说明“Feature 未启用”。
2.5 后台 UI 支持 🖥️
在 Web 模块中,需要在模块定义类上添加对 Feature 管理相关模块的依赖,以便后台 UI 能正确加载并渲染功能管理页面。
// 文件:MyApp.Web/MyAppWebModule.cs
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Modularity;
using Volo.Abp.FeatureManagement;
using Volo.Abp.FeatureManagement.Web;namespace MyApp.Web
{[DependsOn(typeof(MyAppApplicationModule),typeof(AbpAspNetCoreMvcModule),typeof(AbpFeatureManagementApplicationModule),typeof(AbpFeatureManagementHttpApiModule),typeof(AbpFeatureManagementWebModule))]public class MyAppWebModule : AbpModule{// 如果想手动注册 Provider,可在 ConfigureServices 中补充public override void ConfigureServices(ServiceConfigurationContext context){// 可选:显式添加 MyAppFeatureDefinitionProvider// Configure<AbpFeatureManagementOptions>(options =>// {// options.DefinitionProviders.Add<MyAppFeatureDefinitionProvider>();// });}}
}
2.5.1 React 前端路由示例
// 文件:src/routes.tsx(React 示例)
import { FeatureManagement } from '@abp/feature-management'; // React 官方包
import { AuthGuard } from '@abp/abp-ui-react';
import HomePage from './pages/HomePage';export const routes = [{path: '/',element: <HomePage />,},{path: '/feature-management',element: <FeatureManagement />,// 只有拥有 AbpFeatureManagement.FeatureManagement.Default 权限才可访问canActivate: [AuthGuard],data: { requiredPolicy: 'AbpFeatureManagement.FeatureManagement.Default' },},// …其他路由
];
🔔 提示:
- React 端需安装
@abp/feature-management
或@abp/react-components
。requiredPolicy
必须与后端在PermissionDefinitionProvider
中定义的权限名称一致,否则会导致页面或菜单无法显示 citeturn1search2。
2.5.2 Angular 前端路由示例
// 文件:app/app-routing.module.ts(Angular 示例)
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FeatureManagementComponent } from '@abp/ng.feature-management';
import { NgxPermissionsGuard } from 'ngx-permissions';const routes: Routes = [{path: '',children: [{path: 'feature-management',component: FeatureManagementComponent,canActivate: [NgxPermissionsGuard],data: { permissions: { only: 'AbpFeatureManagement.FeatureManagement.Default' } },},// …其他路由],},
];@NgModule({imports: [RouterModule.forChild(routes)],exports: [RouterModule],
})
export class AppRoutingModule {}
🔔 提示:
- Angular 端需安装
@abp/ng.feature-management
、ngx-permissions
等依赖。permissions.only
必须与后端在PermissionDefinitionProvider
中定义的权限名称一致,否则会导致页面或菜单无法显示 citeturn1search2。
🔍 三、实战示例
3.1 📘 场景一:PDF 报表开关
- Tenant A:将
EnablePdfReport
设为true
- Tenant B:将
EnablePdfReport
保持false
访问 /api/report/pdf
接口时:
- 🎉 Tenant A 能够正常生成并返回报表
- 🚫 Tenant B 则会被拦截并返回 HTTP 403
3.2 📕 场景二:导出限额控制
在 ApplicationService
中注入 IFeatureChecker
并获取数值型特性值,示例如下:
// 文件:MyApp.Application/ReportAppService.cs
using Volo.Abp.Application.Services;
using Volo.Abp.Features;
using Volo.Abp.Validation;
using Volo.Abp;
using Microsoft.Extensions.Logging;namespace MyApp
{public class ReportAppService : ApplicationService{private readonly IFeatureChecker _featureChecker;private readonly ILogger<ReportAppService> _logger;public ReportAppService(IFeatureChecker featureChecker,ILogger<ReportAppService> logger){_featureChecker = featureChecker;_logger = logger;}public async Task ExportAsync(int count){int limit;try{// 泛型方法会自动将字符串值转换为 int,若非法则抛出 AbpValidationExceptionlimit = await _featureChecker.GetAsync<int>(MyAppFeatures.ExportLimit);}catch (AbpValidationException ex){_logger.LogWarning(ex, "🚧 ExportLimit 非法,使用默认值 100。");limit = 100;}if (count > limit){throw new BusinessException("❌ 超出导出限额");}// 导出逻辑await Task.CompletedTask;}}
}
ℹ️ 说明:
- 若直接使用
GetOrNullAsync
返回string
,则需手动int.TryParse
并处理异常;推荐使用泛型GetAsync<int>
搭配NumericValueValidator
,由框架自动校验 citeturn1search2。- 业务高峰期可通过 Feature 界面临时调整限额,无需重启服务,响应快速。
🔧 四、扩展内容
4.1 🌐 本地化资源支持
将资源文件放在 MyApp.Domain.Shared/Localization/MyAppResource.xml
,示例如下:
<!-- 文件:MyApp.Domain.Shared/Localization/MyAppResource.xml -->
<localization xmlns="https://docs.abp.io/en/abp/latest/Localization/Model"><texts><text name="MyApp" value="我的应用" /><text name="EnablePdfReport" value="启用 PDF 报表" /><text name="ExportLimit" value="导出限额" /><text name="Permission:FeatureManagement" value="功能管理" /><text name="Permission:FeatureManagement:Default" value="访问功能管理界面" /></texts>
</localization>
若需要多语言支持,可在同目录下添加 MyAppResource.en.xml
、MyAppResource.zh-CN.xml
等对应文件;确保项目已在模块中启用本地化:
// 文件:MyApp.Domain.Shared/MyAppDomainSharedModule.cs
using Volo.Abp.Localization;
using Volo.Abp.Modularity;namespace MyApp
{public class MyAppDomainSharedModule : AbpModule{public override void ConfigureServices\ServiceConfigurationContext context){Configure<AbpLocalizationOptions>(options =>{options.Resources.Get<MyAppResource>().AddBaseTypes(typeof(AbpValidationResource)).AddVirtualJson("/Localization/MyApp");});}}
}
⚠️ 注意:
- 目录结构必须与
AddVirtualJson("/Localization/MyApp")
中的路径保持一致。- 若本地化资源文件放在不同位置,需要同步修改
AddVirtualJson
的参数。
4.2 🖥️ UI 模块接入说明
-
React 端
- 安装依赖:
npm install @abp/feature-management @abp/abp-ui-react
- 在
routes.tsx
中添加如下路由:import { FeatureManagement } from '@abp/feature-management'; import { AuthGuard } from '@abp/abp-ui-react'; import HomePage from './pages/HomePage';export const routes = [{path: '/',element: <HomePage />,},{path: '/feature-management',element: <FeatureManagement />,canActivate: [AuthGuard],data: { requiredPolicy: 'AbpFeatureManagement.FeatureManagement.Default' },},// …其他路由 ];
- 安装依赖:
-
Angular 端
- 安装依赖:
npm install @abp/ng.feature-management ngx-permissions
- 在
app-routing.module.ts
中添加如下路由:import { FeatureManagementComponent } from '@abp/ng.feature-management'; import { NgxPermissionsGuard } from 'ngx-permissions';const routes: Routes = [// …其他路由{path: 'feature-management',component: FeatureManagementComponent,canActivate: [NgxPermissionsGuard],data: { permissions: { only: 'AbpFeatureManagement.FeatureManagement.Default' } },}, ];
- 安装依赖:
💡 提示:
- React 与 Angular 示例要区分清楚,避免包名或组件名混淆。
- 确保前端依赖包版本与后端 ABP 版本兼容。
4.3 🎨 灰度流程图
4.4 🧪 单元测试
在测试项目中,先通过 MyAppTestBase
(ABP 提供的测试基类)或构造函数注入获取所需仓储与服务实例。例如:
// 文件:MyApp.Tests/FeatureManagementTests.cs
using Volo.Abp.FeatureManagement;
using Volo.Abp.Features;
using Volo.Abp.Testing;
using Xunit;namespace MyApp.Tests
{public class FeatureManagementTests : MyAppTestBase{private readonly IFeatureValueRepository _featureValueRepository;private readonly ReportAppService _reportAppService;public FeatureManagementTests(){// 通过基类方法解析依赖_featureValueRepository = GetRequiredService<IFeatureValueRepository>();_reportAppService = GetRequiredService<ReportAppService>();}[Fact]public async Task GeneratePdfReport_ShouldThrow_WhenFeatureDisabled(){// 准备租户上下文,假设测试基类已创建默认租户var tenantId = CurrentTenant.Id ?? 1;using (CurrentTenant.Change(tenantId)){// 插入特性值:禁用 PDF 报表await _featureValueRepository.InsertAsync(new FeatureValue{Name = MyAppFeatures.EnablePdfReport,ProviderName = FeatureValueProviderName.Tenant, // ABP v9 中定义的常量ProviderKey = tenantId.ToString(),Value = "false"});}await Assert.ThrowsAsync<AbpAuthorizationException>(async () =>{await _reportAppService.GeneratePdfReportAsync();});}}
}
ℹ️ 说明:
- 测试类继承自
MyAppTestBase
后,可以直接使用GetRequiredService<T>()
获取IFeatureValueRepository
、ReportAppService
等。- 确保测试环境中至少存在一个租户,否则
CurrentTenant.Id
可能为空。可以在测试初始化时创建一个租户并切换上下文。FeatureValueProviderName.Tenant
是 ABP v9 中提供的常量;也可直接使用"Tenant"
。