EF Core 6+GraphQL单库多租户全局过滤器实现方案咨询
EF Core 6 无耦合动态租户全局查询过滤器实现方案
核心思路是通过租户解析抽象层隔离数据层与Web层的依赖,不需要让EF Core项目直接引用Microsoft.AspNetCore.Http,也不需要动态重建EF Core模型。
第一步:在数据访问层定义租户解析抽象
不要在数据层引入任何AspNetCore相关依赖,只定义一个和具体解析逻辑无关的接口,数据层所有逻辑只依赖这个抽象:
// 该接口放在EF Core所在的数据访问项目,无任何外部Web框架依赖 public interface ITenantProvider { Guid? CurrentTenantId { get; } }
所有需要做租户隔离的实体统一实现标记接口,方便后续批量配置过滤器:
public interface ITenantEntity { Guid TenantID { get; set; } }
第二步:配置EF Core全局查询过滤器
OnModelCreating确实只会在DbContext初始化时执行一次,但全局过滤器的表达式是每次查询执行时实时求值的,只要表达式里捕获的是可动态取值的服务实例,完全不需要动态重建模型就能适配每个请求变化的租户ID。
在DbContext中通过构造函数注入ITenantProvider,统一配置所有多租户实体的过滤规则:
public class AppDbContext : DbContext { private readonly ITenantProvider _tenantProvider; public AppDbContext(DbContextOptions<AppDbContext> options, ITenantProvider tenantProvider) : base(options) { _tenantProvider = tenantProvider; } public DbSet<Advertise> Advertises { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // 批量给所有实现ITenantEntity的实体配置租户过滤器 foreach (var entityType in modelBuilder.Model.GetEntityTypes() .Where(e => typeof(ITenantEntity).IsAssignableFrom(e.ClrType))) { modelBuilder.Entity(entityType.ClrType) .HasQueryFilter(BuildTenantFilterExpression(entityType.ClrType, _tenantProvider)); } } // 构造动态过滤表达式:e => e.TenantID == _tenantProvider.CurrentTenantId private static LambdaExpression BuildTenantFilterExpression(Type entityType, ITenantProvider tenantProvider) { var parameter = Expression.Parameter(entityType, "e"); var tenantIdProperty = Expression.Property(parameter, nameof(ITenantEntity.TenantID)); var currentTenantIdValue = Expression.Property( Expression.Constant(tenantProvider), nameof(ITenantProvider.CurrentTenantId) ); var equalCheck = Expression.Equal(tenantIdProperty, currentTenantIdValue); return Expression.Lambda(equalCheck, parameter); } }
很多开发者遇到过滤器租户值不更新的问题,本质是错误地在
OnModelCreating执行阶段就把当时固定的租户ID值写死在了表达式里,而非引用可动态取值的服务实例。上述写法捕获的是Scoped生命周期的租户提供器实例,每次查询执行时都会实时读取当前请求的租户ID,兼容DbContext池化场景。
第三步:在Web层实现租户解析逻辑
只有Web/API层需要引用AspNetCore相关包,实现ITenantProvider接口,负责从HTTP请求头读取租户ID,这部分逻辑完全不会侵入数据层:
// 该类放在Web/GraphQL接口项目,负责处理HTTP相关的租户解析逻辑 public class HttpHeaderTenantProvider : ITenantProvider { private readonly IHttpContextAccessor _httpContextAccessor; private const string TenantHeaderKey = "TenantID"; public HttpHeaderTenantProvider(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Guid? CurrentTenantId { get { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) return null; // 从请求头读取租户ID,建议这里加校验逻辑,对比JWT中的租户声明和请求头是否一致,防止伪造 if (httpContext.Request.Headers.TryGetValue(TenantHeaderKey, out var tenantIdStr) && Guid.TryParse(tenantIdStr, out var tenantId)) { return tenantId; } // 可在这里抛出租户ID缺失/非法的异常,终止请求 return null; } } }
第四步:依赖注入注册
在Web项目的启动配置中注册对应服务即可,不需要修改数据层的任何代码:
// 注册HttpContext访问器 builder.Services.AddHttpContextAccessor(); // 注册租户提供器,Scoped生命周期,每个请求独立实例 builder.Services.AddScoped<ITenantProvider, HttpHeaderTenantProvider>(); // 注册DbContext,开启池化也不会影响过滤器动态取值 builder.Services.AddDbContextPool<AppDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); });
落地注意事项
- 所有需要租户隔离的实体都要实现
ITenantEntity接口,会自动应用过滤规则,不需要在业务查询中重复写Where(a => a.TenantID == x)的条件 - 超级管理员跨租户查询等需要绕过过滤器的场景,直接使用EF Core原生的
IgnoreQueryFilters()方法即可 - 如果用GraphQL,需要确认字段解析管道的生命周期和Scoped服务一致,避免租户ID串值;务必在请求管道中校验请求头的TenantID和JWT中携带的租户身份是否匹配,防止恶意构造请求头跨租户访问数据
- 后续如果需要切换租户ID的来源(比如从JWT Claim、路由参数、消息队列元数据中读取),只需要在对应外层项目新增一个
ITenantProvider的实现即可,数据层代码完全不需要改动,从根源上避免跨层耦合。
内容的提问来源于stack exchange,提问作者Rafael Cronemberger
相关产品推荐
相关产品推荐

