EF Core全局查询过滤器Nullable属性空引用异常及动态适配问题
问题描述
我用EF Core全局查询过滤器,基于LegalEntityId过滤数据库记录。核心逻辑是:JWT令牌里存在LegalEntityId时,仅返回关联的记录;LegalEntityId通过ILegalEntityProvider获取,按以下代码应用全局过滤器。
现在遇到的问题:当LegalEntityId为null时,EF Core全局查询过滤器在启动时仅编译一次,无法在运行时适配LegalEntityId的动态变化——过滤器在模型创建阶段就固定了逻辑,后续所有查询都会沿用这个静态逻辑,导致空引用异常或过滤行为不符合预期。
这个场景类似多租户实现,但LegalEntityId支持为null,求可行的解决办法。
相关代码
AppDbContext 实现
public partial class AppDbContext(DbContextOptions<AppDbContext> options, ILegalEntityProvider legalEntityProvider) : DbContext(options) { public Guid? LegalEntityId { get; } = legalEntityProvider.LegalEntity; protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); // 为所有实现IMultiLegalEntity的实体应用全局查询过滤器。 // 这确保查询仅返回与当前LegalEntityId关联的记录, // 该LegalEntityId由用户身份中的'extension_LegalEntityId'声明确定。 // LegalEntityId通过LegalEntityProvider获取,仅匹配LegalEntityId的记录会被包含在查询结果中。 // 全局查询过滤器会在启动时编译,除非设计为条件式,否则无法动态关闭。 modelBuilder.SetQueryFilterOnAllEntities<IMultiLegalEntity>( x => LegalEntityId.HasValue && x.LegalEntities.Any(le => le.Id == LegalEntityId.Value) ); } }
ModelBuilder 扩展方法
public static class ModelBuilderExtensions { /// <summary> /// 为<see cref="ModelBuilder"/>中指定类型的所有实体应用全局查询过滤器。 /// </summary> /// <typeparam name="TEntity">要应用过滤器的实体类型,必须为类或接口类型。</typeparam> /// <param name="builder">要应用全局过滤器的<see cref="ModelBuilder"/>实例。</param> /// <param name="filter">表示要应用于实体的过滤器的<see cref="Expression{Func{TEntity, bool}}"/>。</param> /// <returns>已应用全局查询过滤器的<see cref="ModelBuilder"/>实例。</returns> /// <remarks> /// 此方法将指定过滤器应用于所有可分配给<typeparamref name="TEntity"/>类型的实体。 /// 如果实体已存在查询过滤器,则新过滤器会与现有过滤器通过AND操作组合。 /// </remarks> public static ModelBuilder SetQueryFilterOnAllEntities<TEntity>( this ModelBuilder builder, Expression<Func<TEntity, bool>> filter) { // 可分配给TEntity泛型的实体 var entities = builder.Model.GetEntityTypes() .Where(entity => typeof(TEntity).IsAssignableFrom(entity.ClrType)) .ToList(); foreach (var entity in entities) { // 创建当前实体实际类型的ParameterExpression var entityParam = Expression.Parameter(entity.ClrType, "entity"); // 将参数替换为实际的实体参数(例如 => (entityParameter => ....)) var filterBody = ReplacingExpressionVisitor.Replace(filter.Parameters[0], entityParam, filter.Body); // 获取当前实体的现有查询过滤器 var existingFilter = entity.GetQueryFilter(); if (existingFilter is not null) { // 已存在其他过滤器,将它们组合 filterBody = ReplacingExpressionVisitor.Replace(entityParam, existingFilter.Parameters[0], filterBody); filterBody = Expression.AndAlso(existingFilter.Body, filterBody); // 为组合后的过滤器创建新的lambda表达式 existingFilter = Expression.Lambda(filterBody, existingFilter.Parameters); } else { // 为当前过滤器创建新的lambda表达式 existingFilter = Expression.Lambda(filterBody, entityParam); } // 为实体设置查询过滤器 entity.SetQueryFilter(existingFilter); } return builder; } }
解决方案
核心问题是你原代码中LegalEntityId是构造时赋值的常量,EF Core会把它编译进过滤器,导致运行时无法动态更新。解决思路是让过滤器能动态访问当前上下文的LegalEntityId,并调整逻辑兼容null场景。
1. 修正AppDbContext的LegalEntityId属性
将构造时赋值改为动态获取,确保每次访问都能拿到最新的LegalEntityId:
public partial class AppDbContext(DbContextOptions<AppDbContext> options, ILegalEntityProvider legalEntityProvider) : DbContext(options) { // 动态获取当前请求的LegalEntityId,而非构造时固定值 public Guid? LegalEntityId => legalEntityProvider.LegalEntity; protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); // 调整过滤器逻辑:LegalEntityId为null时返回所有记录,否则仅返回关联数据 modelBuilder.SetQueryFilterOnAllEntities<IMultiLegalEntity>( x => !LegalEntityId.HasValue || x.LegalEntities.Any(le => le.Id == LegalEntityId.Value) ); } }
2. 确保ILegalEntityProvider是请求作用域服务
在DI注册时,将ILegalEntityProvider注册为请求作用域(Scoped),保证每个请求都能获取当前用户的LegalEntityId:
// Program.cs 或 Startup.cs builder.Services.AddScoped<ILegalEntityProvider, LegalEntityProvider>();
3. 逻辑说明
调整后的过滤器逻辑:
- 当
LegalEntityId为null:!LegalEntityId.HasValue为true,条件成立,返回所有符合其他过滤规则的记录。 - 当
LegalEntityId不为null:仅返回LegalEntities集合中包含该Id的记录。
EF Core会在每次查询时,从当前AppDbContext实例获取LegalEntityId的最新值,而非使用启动时的静态常量,完美解决动态适配问题。
额外注意事项
- 若
LegalEntities是导航属性,确保实体配置中正确设置关联关系,避免查询时出现空引用。 - 如需临时禁用过滤器(比如后台批量操作),可使用
IgnoreQueryFilters():var allRecords = dbContext.MyEntities.IgnoreQueryFilters().ToList();
内容的提问来源于stack exchange,提问作者nop
相关产品推荐
相关产品推荐

