乐于分享
好东西不私藏

ABP 多租户源码解读:ICurrentTenant 与自动过滤的实现

ABP 多租户源码解读:ICurrentTenant 与自动过滤的实现

核心接口源码

// framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/ICurrentTenant.cs
public
 interface ICurrentTenant
{
    bool
 IsAvailable { get; }     // 当前租户 ID 是否为 null
    Guid? Id { get; }             // 当前租户 ID,null 表示 Host
    string
? Name { get; }        // 当前租户名称

    IDisposable Change(Guid? id, string? name = null)
;
    // Change 返回 IDisposable,using 块结束后自动恢复上下文

}
// framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/IMultiTenant.cs
public
 interface IMultiTenant
{
    Guid? TenantId { get; }
}

IMultiTenant 是实体标记接口。任何实现了它的实体,仓储操作会自动加上 WHERE TenantId = @currentId

租户切换的实现原理

CurrentTenant.Change 的实现使用 AsyncLocal 存储当前租户 ID,确保同一次请求内共享、跨请求隔离:

// CurrentTenant.cs(简化)
public
 class CurrentTenant : ICurrentTenant, ITransientDependency
{
    private
 readonly ICurrentTenantAccessor _accessor;

    public
 Guid? Id => _accessor.Current?.TenantId;

public IDisposable Change(Guid? id, string? name = null)

    {
        var
 original = _accessor.Current;
        _accessor.Current = new TenantInfo(id, name);
        return
 new DisposeAction(() => _accessor.Current = original); // 恢复原租户
    }
}

DisposeAction 在 using 块结束时自动还原,这是"切换-用完即恢复"模式的关键。

自动过滤的底层

回顾 RepositoryBase.ApplyDataFilters

if (typeof(IMultiTenant).IsAssignableFrom(typeof(TOtherEntity)))
{
    var
 tenantId = CurrentTenant.Id;
    query = (TQueryable)query.WhereIf(
        DataFilter.IsEnabled<IMultiTenant>(),     // 检查过滤是否启用
        e => ((IMultiTenant)e!).TenantId == tenantId
    );
}

DataFilter.IsEnabled<IMultiTenant>() 默认返回 true。可以通过 DataFilter.Disable<IMultiTenant>() 临时禁用:

// 管理员查看所有租户数据
using
 (DataFilter.Disable<IMultiTenant>())
{
    var
 allOrders = await _orderRepo.GetListAsync();  // 不加 TenantId 过滤
}

连接字符串解析

// IConnectionStringResolver.cs — 连接串解析器
// 默认实现:所有租户使用同一个连接串(共享数据库模式)

// 扩展实现:按租户 ID 返回不同的连接串(独立数据库模式)

在独立数据库模式中,每个租户的 Tenant 表里存储了各自的连接串,解析器根据当前 ICurrentTenant.Id 查找对应连接串。

解析器的扩展架构

ABP 的租户解析使用"贡献者"模式:

// 内置解析器(按优先级排列)
// 1. DomainTenantResolveContributor  — 从域名解析(sub.yourapp.com)

// 2. HeaderTenantResolveContributor   — 从请求头解析(X-Tenant-Id)

// 3. CookieTenantResolveContributor   — 从 Cookie 解析

// 4. RouteTenantResolveContributor    — 从路由参数解析

// 5. QueryStringTenantResolveContributor — 从 URL 参数解析

自定义解析:

Configure<AbpTenantResolveOptions>(options =>
{
    options.Contributors.Add(new MyCustomTenantResolveContributor());
});

实战:三选一的数据隔离方案

// 方案 1:共享数据库(默认)——最简单
// 所有租户共用表,每行数据记录 TenantId

// 配置:不需要额外配置

// 适合:B2B 小规模 SaaS


// 方案 2:独立 Schema——中等隔离

// 同一数据库,每个租户一个 Schema,表结构相同

// 配置:重写 IConnectionStringResolver


// 方案 3:独立数据库——最高隔离

// 每个租户独立的数据库实例

// 配置:Tenant 实体存储连接串,IConnectionStringResolver 按 TenantId 返回不同串