租户识别必须在userouting后、useauthentication前的中间件中完成,通过host/路径/head提取tenantid并存入httpcontext.items;dbcontext需按租户动态构建options并注册为scoped工厂;数据过滤必须用全局查询过滤器闭包租户id;迁移须导出幂等sql后按租户执行。

租户识别必须在请求生命周期早期完成
ASP.NET Core 中租户识别不能拖到 Controller 层才做,否则中间件、授权、日志、EF Core 上下文初始化都可能用错租户上下文。典型错误是把 TenantId 从路由或 Header 里取出来后,在 Action 里才塞进 DbContext,这时 IServiceProvider 已经解析完毕,IDbContextFactory 或作用域内的 DbContext 实例早已创建完毕,隔离彻底失效。
正确做法是在 UseRouting 之后、UseAuthentication 之前插入自定义中间件,从域名(如 tenant1.example.com)、路径前缀(如 /t/tenant1/api)或 X-Tenant-ID Header 提取标识,并写入 HttpContext.Items 或 AsyncLocal<string></string>。注意:别用静态字段存租户 ID,会跨请求污染。
- 推荐优先用 Host 头匹配,符合 SaaS 常见部署模式,且对客户端透明
- 若用路径前缀,需确保
MapControllers前已剥离前缀,否则路由失败 - Header 方式适合内部调用,但无法防御伪造,必须配合网关校验或 JWT 声明
DbContext 必须按租户动态切换连接字符串
硬编码一个 ConnectionString 或在 appsettings.json 里写死多个连接字符串,都不是可持续方案。EF Core 不支持运行时替换 DbContextOptions 的连接字符串——一旦 DbContext 实例化,Database.GetDbConnection().ConnectionString 是只读的。强行修改会导致连接复用混乱、事务跨库等严重问题。
真正可行的是为每个租户提供独立的 DbContextOptions 实例,靠依赖注入容器按租户键解析。关键点在于注册方式:不能用 AddDbContext<appdbcontext></appdbcontext> 单例/作用域注册,而要用 AddScoped<appdbcontext>(sp => { ... })</appdbcontext> 工厂模式,在工厂函数里根据当前租户动态构建 DbContextOptionsBuilder。
- 连接字符串应从安全存储(如 Azure Key Vault、HashiCorp Vault)按租户 ID 动态拉取,避免明文写在配置中
- 务必启用连接池隔离:SQL Server 需在连接字符串加
Application Name=tenant-{id},PostgreSQL 用ApplicationName=tenant-{id},便于监控和限流 - 如果租户数超千级,避免为每个租户预建
DbContextOptions,改用缓存 +ConcurrentDictionary<string dbcontextoptions></string>
数据过滤必须用全局查询过滤器(Global Query Filters)
靠开发人员手动在每个 LINQ 查询里写 .Where(x => x.TenantId == currentTenantId),等于没隔离——漏写一处就全盘崩塌。EF Core 的 HasQueryFilter 是唯一靠谱的兜底机制,它会在所有查询(包括 Include 关联、原始 SQL 以外的 LINQ)自动注入 WHERE 条件。
但要注意:过滤器表达式必须是可翻译成 SQL 的纯函数,不能调用 GetTenantIdFromContext() 这类运行时方法。正确姿势是把租户 ID 存进 DbContext 构造参数,再在 OnModelCreating 中捕获该值并闭包进过滤器。
- 软删除字段(如
IsDeleted)和租户过滤必须同时生效,写成x => x.TenantId == _tenantId && !x.IsDeleted - 过滤器对
IgnoreQueryFilters()开放,测试或后台管理需要绕过时,得明确允许且记录审计日志 - 导航属性(如
Order.Items)也会被自动过滤,但若关联表没租户字段,需用ThenInclude+ 显式 Join 补救
迁移与种子数据必须按租户分治
dotnet ef migrations add 只能生成通用结构变更脚本,不能处理租户专属数据(如租户默认角色、计费策略)。更危险的是,直接运行 Update-Database 会把迁移应用到默认连接字符串指向的数据库,大概率不是目标租户库。
解决方案是放弃全自动迁移执行,改用“迁移脚本导出 + 按租户批量执行”。用 dotnet ef migrations script --idempotent --output migration.sql 生成幂等 SQL,再通过后台任务或运维脚本,遍历租户列表,用对应连接字符串执行该 SQL。
- 种子数据(如系统字典)应拆分为“平台级”和“租户级”,前者走单次部署,后者用租户激活事件触发异步初始化
- 避免在
DbContext.OnModelCreating里调用ModelBuilder.Seed(),它会在设计时执行,导致多租户环境生成错误模型 - 若用 EF Core 8+,可结合
IDesignTimeDbContextFactory实现租户感知的迁移设计时上下文,但生产运行时仍需手动调度
租户上下文不是装饰器,是贯穿请求、连接、查询、迁移的完整链路。任何一个环节脱钩,隔离就变成幻觉。











