EF Core租户查询过滤器扩展方法切换租户失效问题求助
问题现象
在EF Core实现租户查询过滤器时,最初在ApplicationDbContext内编写私有方法SetTenantQueryFilter,生成的SQL会将CurrentUserId作为参数传递,切换租户功能正常。但将该逻辑封装为ModelBuilder的扩展方法后,生成的SQL直接硬编码租户ID,导致切换租户时过滤器未更新,即使尝试从OnModelCreating传入UserService也无效。
原正常代码
public class ApplicationDbContext : DbContext { private readonly IUserService userService; public ApplicationDbContext(DbContextOptions options) : base(options) => userService = DbContextExtension.ServiceCollection.CheckService<IUserService>(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); SetTenantQueryFilter<ClassRoom>(builder); SetTenantQueryFilter<Course>(builder); SetTenantQueryFilter<Demo>(builder); SetTenantQueryFilter<Student>(builder); SetTenantQueryFilter<Teacher>(builder); } private void SetTenantQueryFilter<TEntity>(ModelBuilder builder) where TEntity : class, IUserIsolation => builder.Entity<TEntity>().HasQueryFilter(s => s.TenantId == userService.CurrentUserId); }
原正常代码生成的SQL
Executed DbCommand (2ms) [Parameters=[@__ef_filter__CurrentUserId_0='b00ffd0d-1fd1-ed11-bef1-f8b54d53dd5f' (Size = 255), @__p_0='0', @__p_1='10'], CommandType='Text', CommandTimeout='60'] SELECT [s].[ID], [s].[AcademicYear], [s].[Address], [s].[Age], [s].[AuditId], [s].[ClassRoomId], [s].[CourseId], [s].[Email], [s].[Name], [s].[PhoneNumbers], [s].[SoftDeleteId], [s].[StudentType], [s].[SynchronizeId], [s].[TeacherId], [s].[TenantId], [s].[Url] FROM [Student] AS [s] WHERE [s].[TenantId] = @__ef_filter__CurrentUserId_0 ORDER BY [s].[Name] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
扩展方法代码(有问题的版本)
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.SetTenantQueryFilter<ClassRoom>(); builder.SetTenantQueryFilter<Course>(); builder.SetTenantQueryFilter<Demo>(); builder.SetTenantQueryFilter<Student>(); builder.SetTenantQueryFilter<Teacher>(); } } public static class DbExtension { private static readonly IUserService UserService = DbContextExtension.ServiceCollection.CheckService<IUserService>(); public static void SetTenantQueryFilter<TEntity>(this ModelBuilder builder) where TEntity : class, IUserIsolation => builder.Entity<TEntity>().HasQueryFilter(s => s.TenantId == UserService.CurrentUserId); }
有问题的扩展方法生成的SQL
Executed DbCommand (1ms) [Parameters=[@__p_0='0', @__p_1='10'], CommandType='Text', CommandTimeout='60'] SELECT [s].[ID], [s].[AcademicYear], [s].[Address], [s].[Age], [s].[AuditId], [s].[ClassRoomId], [s].[CourseId], [s].[Email], [s].[Name], [s].[PhoneNumbers], [s].[SoftDeleteId], [s].[StudentType], [s].[SynchronizeId], [s].[TeacherId], [s].[TenantId], [s].[Url] FROM [Student] AS [s] WHERE [s].[TenantId] = N'b00ffd0d-1fd1-ed11-bef1-f8b54d53dd5f' ORDER BY [s].[Name] OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
问题根源
扩展方法中使用了静态只读的IUserService实例,OnModelCreating只会在EF Core模型构建时执行一次,此时会直接获取当时的CurrentUserId值并将其硬编码到查询过滤器表达式中,后续切换租户时,过滤器表达式已经固化,不会再重新获取新的租户ID。
而原正常代码中,userService是ApplicationDbContext的实例字段,每次创建DbContext实例时都会关联对应的IUserService,查询过滤器表达式捕获这个实例字段后,EF Core会将其识别为需要动态求值的成员,每次查询时都会实时获取CurrentUserId的最新值,因此能正常切换租户。
解决方案
方案1:扩展方法接收IUserService实例作为参数
将DbContext中注入的IUserService实例传入扩展方法,确保过滤器捕获的是当前DbContext关联的服务实例:
public class ApplicationDbContext : DbContext { private readonly IUserService _userService; // 推荐通过构造函数注入IUserService,避免直接操作ServiceCollection public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IUserService userService) : base(options) { _userService = userService; } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.SetTenantQueryFilter<ClassRoom>(_userService); builder.SetTenantQueryFilter<Course>(_userService); builder.SetTenantQueryFilter<Demo>(_userService); builder.SetTenantQueryFilter<Student>(_userService); builder.SetTenantQueryFilter<Teacher>(_userService); } } public static class DbExtension { public static void SetTenantQueryFilter<TEntity>(this ModelBuilder builder, IUserService userService) where TEntity : class, IUserIsolation { builder.Entity<TEntity>().HasQueryFilter(s => s.TenantId == userService.CurrentUserId); } }
方案2:从DbContext动态获取IUserService
如果不想在OnModelCreating中传递服务实例,可以让扩展方法通过DbContext的GetService方法动态获取服务:
public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.SetTenantQueryFilter<ClassRoom>(this); builder.SetTenantQueryFilter<Course>(this); builder.SetTenantQueryFilter<Demo>(this); builder.SetTenantQueryFilter<Student>(this); builder.SetTenantQueryFilter<Teacher>(this); } } public static class DbExtension { public static void SetTenantQueryFilter<TEntity>(this ModelBuilder builder, DbContext dbContext) where TEntity : class, IUserIsolation { var userService = dbContext.GetService<IUserService>(); builder.Entity<TEntity>().HasQueryFilter(s => s.TenantId == userService.CurrentUserId); } }
方案效果
两种方案都能让EF Core识别到userService.CurrentUserId是需要动态求值的成员,每次执行查询时都会获取最新的租户ID,生成的SQL会将其作为参数传递,而非硬编码固定值,切换租户后过滤器会自动生效。
内容的提问来源于stack exchange,提问作者Kannan Chithamabaranathan

