🚀 功能管理:基于 ABP 的 Feature Management 实现动态开关
📚 目录
- [🚀 功能管理:基于 ABP 的 Feature Management 实现动态开关](#🚀 功能管理:基于 ABP 的 Feature Management 实现动态开关)
-
- [📚 一、背景分析](#📚 一、背景分析)
- [🧩 二、核心功能设计](#🧩 二、核心功能设计)
-
- [2.1 定义 Feature 常量与分组](#2.1 定义 Feature 常量与分组)
- [2.2 实现 FeatureDefinitionProvider 🛠️](#2.2 实现 FeatureDefinitionProvider 🛠️)
- [2.3 注册到模块 ⚙️](#2.3 注册到模块 ⚙️)
-
- [2.3.1 ABP 特性注册流程图](#2.3.1 ABP 特性注册流程图)
- [2.4 使用 [RequiresFeature] 控制访问 🔒](#2.4 使用 [RequiresFeature] 控制访问 🔒)
- [2.5 后台 UI 支持 🖥️](#2.5 后台 UI 支持 🖥️)
-
- [2.5.1 React 前端路由示例](#2.5.1 React 前端路由示例)
- [2.5.2 Angular 前端路由示例](#2.5.2 Angular 前端路由示例)
- [🔍 三、实战示例](#🔍 三、实战示例)
-
- [3.1 📘 场景一:PDF 报表开关](#3.1 📘 场景一:PDF 报表开关)
- [3.2 📕 场景二:导出限额控制](#3.2 📕 场景二:导出限额控制)
- [🔧 四、扩展内容](#🔧 四、扩展内容)
-
- [4.1 🌐 本地化资源支持](#4.1 🌐 本地化资源支持)
- [4.2 🖥️ UI 模块接入说明](#4.2 🖥️ UI 模块接入说明)
- [4.3 🎨 灰度流程图](#4.3 🎨 灰度流程图)
- [4.4 🧪 单元测试](#4.4 🧪 单元测试)
📚 一、背景分析
在 SaaS 场景中,业务常常要求:
- 🌐 不同租户具备不同功能开关
- 🔀 根据套餐或版本灰度发布新功能
- 🎯 控制功能粒度细化、灵活
ABP 的 Feature Management 模块内建对租户、主机、版本多级别支持,配合后台 UI 可视化管理界面,让开发者无需侵入业务逻辑即可控制功能可用性和灰度发布。
🧩 二、核心功能设计
2.1 定义 Feature 常量与分组
csharp
// 文件: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 扫描范围内的项目中,框架会自动发现并加载。
csharp
// 文件: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/false
group.AddFeature(
MyAppFeatures.EnablePdfReport,
defaultValue: "false",
displayName: LocalizableString.Create<MyAppResource>("EnablePdfReport"),
valueType: new ToggleStringValueType()
);
// 自由文本型,通过 NumericValueValidator 限制数值范围 1~10000
group.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
,可在模块中添加以下配置;否则框架会自动扫描加载,无需再写这段。
csharp
// 文件: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 特性注册流程图
启动应用 框架扫描 FeatureDefinitionProvider 将特性信息加载到数据库 启动完成,UI 与 API 可用
2.4 使用 [RequiresFeature] 控制访问 🔒
以下示例演示如何在 Controller 上使用 [RequiresFeature]
,仅当租户启用对应功能时才允许访问;否则返回 HTTP 403。
csharp
// 文件: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 能正确加载并渲染功能管理页面。
csharp
// 文件: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 前端路由示例
ts
// 文件: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 前端路由示例
ts
// 文件: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
并获取数值型特性值,示例如下:
csharp
// 文件: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,若非法则抛出 AbpValidationException
limit = 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
,示例如下:
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
等对应文件;确保项目已在模块中启用本地化:
csharp
// 文件: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 端
-
安装依赖:
bashnpm install @abp/feature-management @abp/abp-ui-react
-
在
routes.tsx
中添加如下路由:tsimport { 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 端
-
安装依赖:
bashnpm install @abp/ng.feature-management ngx-permissions
-
在
app-routing.module.ts
中添加如下路由:tsimport { 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 🎨 灰度流程图
EnablePdfReport == true EnablePdfReport == false ExportLimit 返回数值 count <= limit count > limit 客户端请求 API IFeatureChecker 检查功能 租户配置 正常执行业务逻辑(生成报表) 返回 403 Forbidden 根据限额判断是否可导出 继续执行导出 抛出 BusinessException
4.4 🧪 单元测试
在测试项目中,先通过 MyAppTestBase
(ABP 提供的测试基类)或构造函数注入获取所需仓储与服务实例。例如:
csharp
// 文件: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"
。