🚀 ABP vNext 多租户开发实战指南
🛠️ 环境:.NET 8.0 + ABP vNext 8.1.5 (C# 11, EF Core 8)
📚 目录
🏠 一、什么是多租户?
多租户 (Multi-Tenancy) 是一种软件架构模式,使一个应用程序可为多个租户服务,同时隔离各自数据。
常见的三种隔离方式:
隔离模型 | 说明 |
---|---|
🏢 单库共享 | 所有租户使用同一套表,通过 TenantId 区分 |
🗃️ 单库分表 | 每个租户独立一套表结构 |
🏛️ 多数据库 | 每个租户单独数据库实例,隔离最强 |
📦 二、ABP 多租户的核心机制
- 🧩 Tenant 实体:核心领域模型。
- 🔄 ICurrentTenant 接口:获取/切换当前租户上下文。
- 🛠️ ITenantResolveContributor:自定义解析器,支持子域名、Header 等。
- 🔒 IDataFilter:自动为查询加上 TenantId 过滤。
- 📦 模块依赖:
[DependsOn(
typeof(AbpTenantManagementDomainModule),
typeof(AbpTenantManagementApplicationModule)
)]
public class MyAppModule : AbpModule { }
// IDataFilter 自动过滤 TenantId
var list = await _repository.Where(e => e.IsActive).ToListAsync();
💡✨ 小贴士:核心机制不只在数据层,还体现在中间件和租户上下文控制。
🚀 三、快速上手:启用多租户支持
// Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Volo.Abp;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Modularity;
var builder = WebApplication.CreateBuilder(args);
// 1. 启用 ABP 与多租户
builder.Services.AddAbp<MyAppModule>(options =>
{
// ⭐ 如需替换默认解析器,可在此处注入 CustomTenantResolver
// options.Services.Replace(ServiceDescriptor.Singleton<ITenantResolveContributor, CustomTenantResolver>());
});
// 2. 构建并初始化
var app = builder.Build();
await app.InitializeAsync();
// 3. 安全中间件
app.UseHttpsRedirection();
app.UseHsts();
// 4. 路由与多租户
app.UseRouting();
app.UseMultiTenancy(); // 🛡️ 必须在身份验证/授权之前
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
// 5. 运行
await app.RunAsync();
// appsettings.json
{
"MultiTenancy": {
"IsEnabled": true
},
"ConnectionStrings": {
"TenantDb": "Server=.;Database=Tenant_{TENANT_ID};User Id={{USER}};Password={{PASSWORD}};"
},
"AllowedTenants": [
"tenant1-id",
"tenant2-id"
]
}
// 自定义租户解析器
using System.Text.RegularExpressions;
using Volo.Abp.MultiTenancy;
using Volo.Abp;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
public class CustomTenantResolver : ITenantResolveContributor
{
public string Name => "CustomHeader";
private readonly IDistributedCache _cache;
private readonly IConfiguration _configuration;
public CustomTenantResolver(IDistributedCache cache, IConfiguration configuration)
{
_cache = cache;
_configuration = configuration;
}
public async Task<string> ResolveAsync(ITenantResolveContext context)
{
var header = context.HttpContext.Request.Headers["Tenant"];
if (string.IsNullOrEmpty(header) || !IsValidTenant(header))
throw new UserFriendlyException("❌ 无效租户");
return header;
}
private bool IsValidTenant(string header)
{
// 简单格式校验
if (header.Length > 36 || !Regex.IsMatch(header, @"^[0-9A-Za-z\-]+$"))
return false;
// 从缓存或配置读取白名单
var whitelist = _cache.GetOrCreate("TenantWhitelist", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return _configuration.GetSection("AllowedTenants").Get<List<string>>();
});
return whitelist.Contains(header);
}
}
// 在模块中注册解析器
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Modularity;
[DependsOn(typeof(AbpTenantManagementDomainModule))]
public class MyAppModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Replace(
ServiceDescriptor.Singleton<ITenantResolveContributor, CustomTenantResolver>()
);
}
}
💡✨ 小贴士:推荐结合 Header + 子域名解析,适配多端生产场景。
🔄 四、ICurrentTenant 用法详解
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
public class MyService : ITransientDependency
{
private readonly ICurrentTenant _currentTenant;
public MyService(ICurrentTenant currentTenant) => _currentTenant = currentTenant;
[UnitOfWork]
public async Task DoSomethingAsync()
{
var tid = _currentTenant.Id;
using (_currentTenant.Change(tid))
{
// 在特定租户上下文中执行逻辑
}
}
}
💡✨ 小贴士:Id == null
表示主机环境;可配合 IUnitOfWork
在后台任务中切换上下文。
🗄️ 五、数据库策略与配置建议
模式 | 说明 | 示例代码 |
---|---|---|
🔖 单库共享 | 通过 TenantId 分类 | context.Set<T>().Where(e => e.TenantId == _currentTenant.Id).ToListAsync(); |
🔑 多数据库 | 每租户动态连接切换 | services.Replace(ServiceDescriptor.Singleton<IConnectionStringResolver, MyConnResolver>()); |
// 自定义 ConnectionStringResolver
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Data;
using Microsoft.Extensions.Configuration;
public class MyConnResolver : DefaultConnectionStringResolver, ITransientDependency
{
private readonly ICurrentTenant _currentTenant;
private readonly IConfiguration _configuration;
public MyConnResolver(IConfiguration configuration, ICurrentTenant currentTenant)
: base(configuration)
=> (_configuration, _currentTenant) = (configuration, currentTenant);
public override string Resolve(string name = null)
{
var template = _configuration["ConnectionStrings:TenantDb"];
var id = _currentTenant.Id?.ToString() ?? "Host";
return template.Replace("{TENANT_ID}", id);
}
}
💡✨ 小贴士:可将租户列表缓存到 IDistributedCache
或内存中,避免频繁访问数据库。
🐞 六、常见问题与解决方案
1. 🚫 无租户上下文
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
public class TenantBackgroundService : BackgroundService, ITransientDependency
{
private readonly ICurrentTenant _currentTenant;
private readonly ITenantManager _tenantManager;
private readonly IMyBusinessService _myBusinessService;
public TenantBackgroundService(
ICurrentTenant currentTenant,
ITenantManager tenantManager,
IMyBusinessService myBusinessService)
{
_currentTenant = currentTenant;
_tenantManager = tenantManager;
_myBusinessService = myBusinessService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var tenants = await _tenantManager.GetListAsync(stoppingToken);
foreach (var tenant in tenants)
{
if (stoppingToken.IsCancellationRequested)
break;
await using (_currentTenant.Change(tenant.Id))
{
await _myBusinessService.ProcessTenantDataAsync(stoppingToken);
}
}
}
}
2. 🧩 缓存污染
var key = $"product:{_currentTenant.Id}:{id}";
3. 🔗 链路追踪
builder.Services.AddOpenTelemetryTracing(b =>
b.AddAspNetCoreInstrumentation()
.AddSqlClientInstrumentation()
.AddJaegerExporter()
);
4. 🔄 多数据库迁移批量处理
var tenants = await tenantManager.GetListAsync();
foreach (var tenant in tenants)
{
using (_currentTenant.Change(tenant.Id))
{
await databaseMigrationService.MigrateAsync();
}
}
💡✨ 小贴士:建议将迁移操作作为独立运维命令或 CI/CD 作业执行,避免在应用启动时触发。
❤️🩹 七、健康检查
// 在 Program.cs 中
builder.Services.AddHealthChecks()
.AddDbContextCheck<MyDbContext>("TenantDb")
.AddUrlGroup(new Uri("https://your-app/health"), name: "AppEndpoint");
app.MapHealthChecks("/health");
💡✨ 小贴士:结合 Prometheus/Grafana 定时监控,提前设置告警阈值。
🔗 八、流程图:ABP 多租户请求流程
🔚 九、总结
💡✨ 小贴士:
- 🏁 项目初期即明确隔离模型,避免后期大改架构。
- ✅ 上线前务必在主机和各租户环境进行全链路测试,确保无遗漏。
- ⚙️ 结合缓存、健康检查与链路追踪,可大幅提升多租户系统性能与可观察性。