多租户
多租户(preview)是常见的系统架构之一,模板对此进行了一定的支持。
考虑以下场景:
我有1000+个普通租户,每个租户的数据量没有很多,没有强资源隔离,属于成本敏感型客户;
我有10+个大客户租户,每个租户的数据量非常大,且有资源隔离的需求,以稳定为首要目标,成本不敏感;
租户模型与配置
在AppHost下的appsettings.Development.json中,有以下配置项:
"Components": { // memory/redis/hybrid "Cache": "Hybrid", // SqlServer/PostgreSQL "Database": "PostgreSQL", // 租户配置;不会关闭租户感知模型 "IsMultiTenant": true }
当前ApiStandard统一使用租户感知的数据模型,模板默认将IsMultiTenant设为true。即使将它改为false,也会创建Tenant表,普通业务实体也会关联TenantId并应用全局过滤器;单租户启动时同样会初始化默认Tenant。AppHost会将该配置作为Components__IsMultiTenant传给服务,但它不关闭TenantId字段、全局过滤器或保存校验。
实施多租户
框架默认兼容多租户,你可以像平常一样编写业务代码。需要关注的是IUserContext、TenantResolutionMiddleware和AppDbFactory之间的配合。
UserContext会从当前用户的 Token Claims 中读取tenant_id和tenant_type,并填充到IUserContext.TenantId和IUserContext.TenantType。- 登录令牌可以包含一个或多个
role_idGUID Claims;IUserContext.RoleIds会读取有效 ID、忽略空值和无效值并移除重复项。模板不会替登录端签发这些 Claims。 TenantResolutionMiddleware始终会在认证之后运行,根据IUserContext.TenantId查询Tenant并缓存到内存中;认证用户没有有效TenantId或Tenant不可用时,请求会被拒绝。AppDbFactory在创建DbContext时接收租户标识。null仅用于系统租户目录上下文并使用配置中的默认连接;正常租户必须使用非空的TenantId,空GUID或未知租户会直接失败,不会静默回退。已解析租户的DbConnectionString或AnalysisConnectionString为空时,才分别回退到对应默认连接(未配置分析默认连接时使用默认业务连接)。Tenant是全局租户目录根实体,忽略继承的TenantId,不应用TenantId全局过滤;其他租户实体自动应用当前租户过滤和保存校验。
Tenant.cs实体中包含了租户的基本信息,你可以根据需要进行扩展。
客户端在获取 Token 时,应将TenantId信息包含在内,后续请求中服务端会根据 Token 中的tenant_id来识别租户。
在登录时处理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); // 查询用户 var user = await _dbSet .Where(u => u.Email == dto.Email) .Include(u => u.SystemRoles) .FirstOrDefaultAsync() ?? throw new BusinessException(Localizer.UserNotExists); jwtService.Claims = [ new Claim(CustomClaimTypes.TenantId, tenant.Id.ToString()), new Claim(CustomClaimTypes.TenantType, tenant.Type.ToString()) ]; // 返回Token }
由于登录时还没有 Token,后端需要先根据登录信息识别租户,再把tenant_id和tenant_type写入返回的 Token。后续请求中UserContext会读取这些 Claims,TenantResolutionMiddleware会加载并缓存对应租户信息。
配置TenantId索引
你无需手动为每个实体模型添加TenantId索引,框架会自动为你处理。这样不管是单租户还是多租户模式,都能保证数据的正确性和隔离性。
在通过 EF Core 生成迁移代码时,TenantIndexConvention会在模型最终确定阶段自动处理TenantId索引:实现ITenantEntityBase的实体会自动把TenantId加入索引首列,已经包含该列时不会重复添加。迁移实际由AppHost的AddEFMigrations资源执行,生成迁移使用AdminService作为启动项目。Components__IsMultiTenant仍会被脚本和迁移资源传递,但不再控制索引是否生成。
单租户部署
单租户部署仍然保留Tenant表和业务实体的TenantId字段。这样数据结构、查询过滤、保存校验和授权模型在单租户与多租户之间保持一致,也能确保用户登录后获得TenantId信息。
如果业务确定不需要租户模型,并且愿意脱离模板的默认约定,可以通过以下方式移除:
- 删除
ContextBase.cs中的TenantsDbSet属性; - 修改
EntityBase.cs,使其继承IEntityBase,而不是ITenantEntityBase,并删除TenantId属性;