多租户需在UseRouting后、UseAuthentication前用中间件提取TenantId并存入HttpContext.Items或AsyncLocal;DbContextOptions须按租户动态构造并缓存;HasQueryFilter需Scoped生命周期闭包捕获租户ID;文件路径须用哈希隔离且校验合法性。

多租户不是加个 TenantId 字段就能跑通的,漏传、错传、跨请求污染、连接复用混乱——这些坑全在运行时爆发,且往往线上才暴露。
租户识别必须在 UseRouting 之后、UseAuthentication 之前做中间件
常见错误是把租户解析拖到 Controller 或 Action 里:这时 IDbContextFactory 已经构建完毕,DbContext 实例已注入,再塞 TenantId 就晚了。
- 必须在
UseRouting()后、UseAuthentication()前插入自定义中间件 - 从
HttpContext.Request.Host(推荐)、HttpContext.Request.Path或HttpContext.Request.Headers["X-Tenant-ID"]提取标识 - 写入
HttpContext.Items["TenantId"]或AsyncLocal,绝不能用static int TenantId—— 多线程下必然串租户 - 若用子路径(如
/t/abc/api/orders),需在MapControllers()前用UsePathBase剥离前缀,否则路由匹配失败
DbContextOptions 必须按租户动态构造,不能 runtime 替换连接字符串
DbContextOptions 是只读的,一旦实例化,Database.GetDbConnection().ConnectionString 就不可变。硬改会导致连接池混用、事务跨库、连接泄漏。
- 注册方式必须是
AddScoped工厂模式,每次请求都新建(sp => { ... }) DbContextOptionsBuilder - 连接字符串应从缓存(如
IMemoryCache)或密钥管理服务(Azure Key Vault)按租户 ID 动态拉取 - SQL Server 连接字符串务必加
Application Name=tenant-{id},PostgreSQL 加ApplicationName=tenant-{id},否则监控和限流无法区分租户 - 租户数超 500 时,用
ConcurrentDictionary缓存已构建的 options,避免重复拼接
HasQueryFilter 必须闭包捕获租户 ID,且 _tenantService 不能是 Singleton
HasQueryFilter 是 EF Core 实现逻辑隔离的核心,但它不作用于 FromSqlRaw 或原生 SQL,也不支持异步委托。最常踩的坑是生命周期错配。
- 过滤器必须用同步方法获取租户 ID,例如
_tenantProvider.GetCurrentTenantId(),不能是await _tenantProvider.GetCurrentTenantIdAsync() -
_tenantProvider必须注册为Scoped,若注册成Singleton,会跨请求返回上一个租户的 ID - 所有租户敏感实体(如
Order、Product)都要显式调用HasQueryFilter,EF Core 不会自动推导 - 软删除(
IsDeleted == false)可与租户过滤组合:.HasQueryFilter(x => x.TenantId == _tenantId && !x.IsDeleted) - 后台任务或迁移脚本需临时绕过过滤,用
context.Set,但绝不允许在用户请求中暴露该能力().IgnoreQueryFilters()
租户文件路径必须用哈希目录,禁止直接拼接 tenantId 或用户输入
文件系统隔离比数据库更易被绕过。用 tenant123_report.pdf 当文件名,或把文件写进 wwwroot/tenant123/,等于给攻击者递刀。
- 租户根目录名必须由系统生成不可预测哈希,例如
Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(tenantId.ToString()))).Substring(0, 16) - 物理路径必须脱离
IWebHostEnvironment.WebRootPath,推荐结构:Path.Combine(Directory.GetCurrentDirectory(), "tenant-data", tenantHash) - 写入前必须校验路径是否落在该租户哈希目录内,拒绝任何含
../、\、空字节的路径段 - 禁止启用静态文件中间件对该目录的映射;下载必须走受控 API(如
GET /api/tenants/{id}/files/{fileId}),并在响应前校验租户权限
真正难的不是写代码,而是让租户上下文在日志、异常、数据库连接、文件 IO、缓存键、HTTP 响应头这些环节全程一致——任何一个环节脱钩,隔离就形同虚设。











