多租户(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即可启用多租户功能。
框架默认支持多租户,你并不需要做太多额外的工作,你可以像平常一样编写代码。
你需要关注的是TenantContext和TenantDbFactory。
在TenantDbFactory中,注入了ITenantContext,可以通过它来获取租户信息,以及独立的租户数据库连接字符串。
Tenant.cs实体中,包含了租户的基本信息,你可以根据需要进行扩展。
客户端在获取Token时,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索引,框架会自动为你处理。这样不管是单租户还是多租户模式,都能保证数据的正确性和隔离性。
在通过EF Core生成迁移代码时,会走MigrationService下的MigrationsModelDifferProxy逻辑,在其中会自动处理TenantId索引。
模板默认是兼容多租户的设计的,即便不启用多租户功能,也会创建Tenant表,实体中也会包含TenantId属性,以便后续启用多租户功能时,能平滑过渡。
如果你确认不使用多租户,也不想创建Tenant表和TenantId属性,可以通过以下方式移除:
ContextBase.cs中的Tenants DbSet属性;EntityBase.cs,使其继承IEntityBase,而不是ITenantEntityBase,并删除TenantId属性;