Blogs
📆 2025-12-19 00:45

多租户

多租户(preview)是常见的系统架构之一,模板对此进行了一定的支持。

考虑以下场景:

我有1000+个普通租户,每个租户的数据量没有很多,没有强资源隔离,属于成本敏感型客户;

我有10+个大客户租户,每个租户的数据量非常大,且有资源隔离的需求,以稳定为首要目标,成本不敏感;

启用多租户

AppHost下的appsettings.Development.json中,有以下配置项:

"Components": {
    // memory/redis/hybrid
    "Cache": "Hybrid",
    // SqlServer/PostgreSQL
    "Database": "PostgreSQL",
    // enable multi-tenant features
    "IsMultiTenant": false
  }

其中IsMultiTenant表示是否启用多租户功能,修改成true即可启用多租户功能。

实施多租户

框架默认支持多租户,你并不需要做太多额外的工作,你可以像平常一样编写代码。

你需要关注的是TenantContextTenantDbFactory

TenantDbFactory中,注入了ITenantContext,可以通过它来获取租户信息,以及独立的租户数据库连接字符串。

Tenant.cs实体中,包含了租户的基本信息,你可以根据需要进行扩展。

客户端在获取Token时,TenantId信息会被包含在内,后续的请求中服务端会根据TenantId来进行租户识别。

在登录时处理TenantId

用户在登录前,后端是无法识别租户的,需要在登录时处理。你可以根据请求来源的域名,登录时的邮箱后缀,或者登录时传递的租户标识等方式来识别租户,并将对应的TenantId包含在Token中返回给客户端。

以下是通过邮箱登录时处理TenantId的示例代码:

// SystemUserManager.cs
public async Task<AccessTokenDto> LoginAsync(SystemLoginDto dto)
{
    var domain = dto.Email.Split("@").Last();
    var tenant =
        await _dbContext.Tenants.Where(t => t.Domain == domain).FirstOrDefaultAsync()
        ?? throw new BusinessException(Localizer.TenantNotExist);
    tenantContext.TenantId = tenant.Id;
    tenantContext.TenantType = tenant.Type.ToString();
    // 查询用户
    var user = await _dbSet
        .Where(u => u.Email == dto.Email)
        .Include(u => u.SystemRoles)
        .FirstOrDefaultAsync() ?? throw new BusinessException(Localizer.UserNotExists);
    // 返回Token
}

其中tenantContext是通过依赖注入获取的ITenantContext实例,由于登录时还没有Token,所以需要手动设置TenantId,以便后续的逻辑中能够正确识别租户。

配置TenantId索引

你无需手动为每个实体模型添加TenantId索引,框架会自动为你处理。这样不管是单租户还是多租户模式,都能保证数据的正确性和隔离性。

在通过EF Core生成迁移代码时,会走MigrationService下的MigrationsModelDifferProxy逻辑,在其中会自动处理TenantId索引。

不使用多租户

模板默认是兼容多租户的设计的,即便不启用多租户功能,也会创建Tenant表,实体中也会包含TenantId属性,以便后续启用多租户功能时,能平滑过渡。

如果你确认不使用多租户,也不想创建Tenant表和TenantId属性,可以通过以下方式移除:

  1. 删除ContextBase.cs中的Tenants DbSet属性;
  2. 修改EntityBase.cs,使其继承IEntityBase,而不是ITenantEntityBase,并删除TenantId属性;