2025-09-19 12:59:06

ABP vNext 多租户开发实战指南

🚀 ABP vNext 多租户开发实战指南

🛠️ 环境:.NET 8.0 + ABP vNext 8.1.5 (C# 11, EF Core 8)

📚 目录

🚀 ABP vNext 多租户开发实战指南🏠 一、什么是多租户?📦 二、ABP 多租户的核心机制🚀 三、快速上手:启用多租户支持🔄 四、ICurrentTenant 用法详解🗄️ 五、数据库策略与配置建议🐞 六、常见问题与解决方案1. 🚫 无租户上下文2. 🧩 缓存污染3. 🔗 链路追踪4. 🔄 多数据库迁移批量处理

❤️‍🩹 七、健康检查🔗 八、流程图:ABP 多租户请求流程🔚 九、总结

🏠 一、什么是多租户?

多租户 (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(options =>

{

// ⭐ 如需替换默认解析器,可在此处注入 CustomTenantResolver

// options.Services.Replace(ServiceDescriptor.Singleton());

});

// 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 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>();

});

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()

);

}

}

💡✨ 小贴士:推荐结合 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().Where(e => e.TenantId == _currentTenant.Id).ToListAsync();🔑 多数据库每租户动态连接切换services.Replace(ServiceDescriptor.Singleton());

// 自定义 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("TenantDb")

.AddUrlGroup(new Uri("https://your-app/health"), name: "AppEndpoint");

app.MapHealthChecks("/health");

💡✨ 小贴士:结合 Prometheus/Grafana 定时监控,提前设置告警阈值。

🔗 八、流程图:ABP 多租户请求流程

🔚 九、总结

💡✨ 小贴士:

🏁 项目初期即明确隔离模型,避免后期大改架构。✅ 上线前务必在主机和各租户环境进行全链路测试,确保无遗漏。⚙️ 结合缓存、健康检查与链路追踪,可大幅提升多租户系统性能与可观察性。