ABP vNext 多租户开发实战指南

🚀 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);
    }
}
合法
非法
客户端请求
AbpTenancyMiddleware
内置解析器列表
是否有 Header Tenant?
CustomTenantResolver
子域名解析
校验 IsValidTenant
设置 ICurrentTenant
抛出 UserFriendlyException
// 在模块中注册解析器
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);
    }
}
应用 MyConnResolver appsettings.json 数据库 Resolve(name) 获取 _currentTenant.Id 读取 TenantDb 模板 返回替换后的连接串 使用该连接串执行业务 应用 MyConnResolver appsettings.json 数据库

💡✨ 小贴士:可将租户列表缓存到 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 系统
获取所有租户列表
循环 for each 租户
_currentTenant.Change(tenant.Id)
执行 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 多租户请求流程

用户请求
AbpTenancyMiddleware
ITenantResolveContributor
ICurrentTenant
数据库切换
DbContext

🔚 九、总结

💡✨ 小贴士:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kookoos

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值