C#怎么实现多租户系统 C#如何在ASP.NET Core中实现SaaS多租户数据隔离架构【架构】

穿越時空

穿越時空

2026-03-29

935人浏览

原创

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

c#怎么实现多租户系统 c#如何在asp.net core中实现saas多租户数据隔离架构【架构】

租户识别必须在请求生命周期早期完成

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.ItemsAsyncLocal<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 条件。

C函数速查手册(CHM版)
C函数速查手册(CHM版)

C函数速查手册(CHM版)

下载

但要注意:过滤器表达式必须是可翻译成 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 实现租户感知的迁移设计时上下文,但生产运行时仍需手动调度

租户上下文不是装饰器,是贯穿请求、连接、查询、迁移的完整链路。任何一个环节脱钩,隔离就变成幻觉。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

2023.06.20

1329

3

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

2023.07.25

1510

9

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

2023.08.02

671

5

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

2023.08.09

551

4

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

2023.09.05

909

5

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

2023.09.20

1127

7

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

2023.09.20

1656

8

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

2023.09.22

5420

6

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

2023.09.22

418

3

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
RabbitMQ 教程手册
RabbitMQ 教程手册

共0课时 | 0人学习

Linux man-pages 项目
Linux man-pages 项目

共0课时 | 0人学习

C# 教程
C# 教程

共94课时 | 20.2万人学习