EF Core 数据过滤

1 前言

本文致力于将一种动态数据过滤的方案描述出来(基于 EF Core 官方的数据筛选器),实现自动注册,多个条件过滤,单条件禁用(实际上是参考ABP的源码),并尽量让代码保持 EF Core 的原使用风格。

1.1 本文的脉络

会在一开始,讲述数据过滤的场景以及基本的实现思路。

随后列出 EF Core 官方的数据查询筛选器例子。

最后将笔者的方案按功能(自动注册,多个条件过滤,单条件禁用)逐一实现出来。

1.2 数据过滤的场景

一般我们会有这样的场景,可能需要数据过滤:

  • 软删
  • 多租户
  • 通用数据权限(数据过滤)

如软删,我们一般会希望,我们查询出来的数据,是过滤掉被删除数据的,可能我们会这样写:

var users = db.User.Where(u => !u.IsDeleted).ToList(); 

但是如果数据过滤全靠人工编写,那会是一件很烦的事,有时候甚至会忘记写。而且如果以后发生了什么需求变化,需要修改数据过滤的代码,到时候是到处修改,也是很烦的一件事。

如果能把数据过滤统一管理起来,这样不但不用重复无意义的工作,而且以后需要修改的时候,改一处地方即可。

2 EF Core 查询筛选器

2.1 介绍

EF Core 官方提供的查询筛选器(Query Filter)能满足我们过滤数据的基本需求,下面介绍一下这种筛选器。

EF Core 官方的查询筛选器,是在 DbContext 的 OnModelCreating 中定义的,且每个实体只能拥有一个筛选器(如定义了多个筛选器,则只会生效最后一个)。

筛选器默认是启用的,如要禁用,需要在查询过程中使用 IgnoreQueryFilters 方法,如:

var users = db.User.IgnoreQueryFilters().ToList(); 

2.2 基本使用

具体可以自行翻查官方文档:全局查询筛选器

(1)定义带有软删字段的实体

public class TestDelete {     public int Id { get; set; }     public string? Name { get; set; }     public bool IsDeleted { get; set; } = false; } 

(2)注册筛选器

使用 HasQueryFilter API 在 OnModelCreating 中配置查询筛选器。

protected override void OnModelCreating(ModelBuilder builder) {     builder.Entity<TestDelete>().HasQueryFilter(e => !e.IsDeleted); } 

(3)查询中使用

直接查询:

var deletes = _context.Set<TestDelete>().ToList(); 

将生成如下SQL:

SELECT [t].[Id], [t].[IsDeleted], [t].[Name] FROM [TestDelete] AS [t] WHERE [t].[IsDeleted] = CAST(0 AS bit) 

查询结果将会过滤掉已删除的数据。

(4)禁用筛选器

使用 IgnoreQueryFilters API 禁用筛选器:

var deletes = _context.Set<TestDelete>()IgnoreQueryFilters().ToList(); 

将生成如下SQL:

SELECT [t].[Id], [t].[IsDeleted], [t].[Name] FROM [TestDelete] AS [t] 

将不会过滤数据。

2.3 限制

EF Core 查询筛选器的限制很明显:

  • 只能生效最后一个
  • 一旦禁用,将禁用所有过滤条件

只能生效最后一个这个,可以通过拼凑多个条件的 Expression 来解决。

禁用过滤器这个,只能通过特定的手段来实现单个条件的禁用了。

3 自定义数据过滤

3.1 目标

将实现这些功能:

  • 自动注册实体、筛选器等

  • 多个条件过滤

  • 单条件禁用

3.2 自动注册实体

完成这个功能以后,将不需要自己一个一个去注册实体,不需要重复以下这句代码:

builder.Entity<TestDelete>(); 

(1)基础实体准备

准备一个 EntityBase 作为所有实体的父类,所有继承该类的非 abstract 类都将被注册为实体。

public abstract class EntityBase {}  public abstract class EntityBase<TKey> : EntityBase     where TKey : struct {     public TKey Id { get; set; } } 

(2)自动注册实体实现

笔者自定义的 DbContext 名为 EDbContext,下面将多次使用到这个上下文。

OnModelCreating 中编写如下代码:

// 获取程序集 Assembly assembly = typeof(EDbContext).Assembly; // 获取所有继承自 EntityBase 的非 abstract 类 List<Type> entityTypes = assembly.GetTypes()     .Where(t => t.IsSubclassOf(typeof(EntityBase)) && !t.IsAbstract)     .ToList();  // 注册实体 foreach(Type entityType in entityTypes) {     builder.Entity(entityType); } 

3.3 自定注册筛选器

完成这个功能以后,将不需要自己一个一个去注册一些筛选器,如:不需要重复以下这句代码:

builder.Entity<TestDelete>().HasQueryFilter(e => !e.IsDeleted); 

(1)基础实体准备

定义一个软删的 interface,所有需要软删功能的实体,都去实现这个接口:

// 定义软删接口 interface ISoftDelete {     public bool IsDeleted { get; set; } } // TestDelete 相应修改为 public class TestDelete : EntityBase<int>, ISoftDelete {     public string? Name { get; set; }     public bool IsDeleted { get; set; } = false; } 

(2)自动注册实现

EDbContext 的代码变为如下(增加了一个 ConfigureFilters 方法):

因为 ConfigureFilters 是一个泛型方法,需要做一些特殊处理。

protected override void OnModelCreating(ModelBuilder builder) {     Assembly assembly = Assembly.GetExecutingAssembly();     List<Type> entityTypes = assembly.GetTypes()         .Where(t => t.IsSubclassOf(typeof(EntityBase)) && !t.IsAbstract)         .ToList();      // 特殊处理:获取 ConfigureFilters     MethodInfo? configureFilters = typeof(EDbContext).GetMethod(         nameof(ConfigureFilters),         BindingFlags.Instance | BindingFlags.NonPublic     );      if (configureFilters == null) throw new ArgumentNullException(nameof(configureFilters));      foreach(Type entityType in entityTypes)     {         builder.Entity(entityType);          // 如果实体实现了 ISoftDelete 接口,则自动注册软删筛选器         if (typeof(ISoftDelete).IsAssignableFrom(entityType))         {             // 特殊处理:调用 ConfigureFilters             configureFilters                 .MakeGenericMethod(entityType)                 .Invoke(this, new object[] { builder });         }     } }  // 自定义配置筛选器方法 protected virtual void ConfigureFilters<TEntity>(ModelBuilder builder)     where TEntity : class {     Expression<Func<TEntity, bool>> expression = e => !EF.Property<bool>(e, "IsDeleted");     builder.Entity<TEntity>().HasQueryFilter(expression); } 

(3) 测试:自动注册功能的测试

完成自动注册以后,运行程序,看看过滤器是否有效果:

直接查询:

var deletes = _context.Set<TestDelete>().ToList(); 

将生成如下SQL:

SELECT [t].[Id], [t].[IsDeleted], [t].[Name] FROM [TestDelete] AS [t] WHERE [t].[IsDeleted] = CAST(0 AS bit) 

查询结果将会过滤掉已删除的数据。

可以看到,自动注册是成功的!

3.4 实现:多个条件过滤

在这一小节中,将会实现多个条件过滤。

一般我们的程序中,除了软删,还可能有其他的需要统一管理的数据过滤,如:多租户。

(1)基础实体准备

准备一个多租户的 interface,命名为 ITenant,所有需要多租户控制的,都实现该接口。

并准备一个 TestTenant 同时实现 ITenant 和 ISoftDelete。

为简单处理,将 TenantId 默认值设置为 1

// 多租户接口 public interface ITenant {     public int TenantId { get; set; } }  public class TestTenant : EntityBase<int>, ITenant, ISoftDelete {     public string? Name { get; set; }     public int TenantId { get; set; } = 1;     public bool IsDeleted { get; set; } = false; } 

(2)合并表达式树代码准备

因为涉及到两个表达式树(Expression)的合并,这里准备了合并的代码(摘自ABP框架),放在 EDbContext 中即可:

关于表达式树,个人也是不会,就不在这里误人子弟啦。

protected virtual Expression<Func<T, bool>> CombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2) {     var parameter = Expression.Parameter(typeof(T));      var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter);     var left = leftVisitor.Visit(expression1.Body);      var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);     var right = rightVisitor.Visit(expression2.Body);      return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left, right), parameter); }  class ReplaceExpressionVisitor : ExpressionVisitor {     private readonly Expression _oldValue;     private readonly Expression _newValue;      public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)     {         _oldValue = oldValue;         _newValue = newValue;     }      public override Expression Visit(Expression? node)     {         if (node == _oldValue)         {             return _newValue;         }          return base.Visit(node)!;     } } 

(3)实现多个条件过滤

EDbContext 的代码变为如下:

修改了 OnModelCreatingConfigureFilters 的大部分代码:

注:为简单处理,租户的过滤条件设置为 TenantId == 1

protected override void OnModelCreating(ModelBuilder builder) {     Assembly assembly = typeof(EDbContext).Assembly;     List<Type> entityTypes = assembly.GetTypes()         .Where(t => t.IsSubclassOf(typeof(EntityBase)) && !t.IsAbstract)         .ToList();      MethodInfo? configureFilters = typeof(EDbContext).GetMethod(         nameof(ConfigureFilters),         BindingFlags.Instance | BindingFlags.NonPublic     );      if (configureFilters == null) throw new ArgumentNullException(nameof(configureFilters));      foreach(Type entityType in entityTypes)     {         // 注册实体         builder.Entity(entityType);          // 注册筛选器         configureFilters             .MakeGenericMethod(entityType)             .Invoke(this, new object[] { builder, entityType });     } }  protected virtual void ConfigureFilters<TEntity>(ModelBuilder builder, Type entityType) 	where TEntity : class {     Expression<Func<TEntity, bool>>? expression = null;      if (typeof(ISoftDelete).IsAssignableFrom(entityType))     {         expression = e => !EF.Property<bool>(e, "IsDeleted");     }      if (typeof(ITenant).IsAssignableFrom(entityType))     {         Expression<Func<TEntity, bool>> tenantExpression = e => EF.Property<int>(e, "TenantId") == 1;         expression = expression == null ? tenantExpression : CombineExpressions(expression, tenantExpression);     }      if (expression == null) return;      builder.Entity<TEntity>().HasQueryFilter(expression); } 

(4)测试:多条件过滤

直接查询:

var tenants = _context.Set<TestTenant>().ToList(); 

将生成如下SQL:

SELECT [t].[Id], [t].[IsDeleted], [t].[Name], [t].[TenantId] FROM [TestTenant] AS [t] WHERE ([t].[IsDeleted] = CAST(0 AS bit)) AND ([t].[TenantId] = 1) 

查询结果将会过滤掉已删除,且租户Id=1的数据。

3.5 实现:单条件禁用

直接使用 IgnoreQueryFilters 将会禁用筛选器,这里希望有个控制,可以单个条件控制:

下面实现禁用软删筛选器:

(1)DbContext 变量控制

在 EDbContext 中新增一个属性:

public class EDbContext : DbContext {     public bool IgnoreDeleteFilter { get; set; } = false;          // 其他代码忽略 } 

(2)修改筛选器

修改了 ConfigureFilters 的代码:

if (typeof(ISoftDelete).IsAssignableFrom(entityType)) {     // 如果 IgnoreDeleteFilter 为 true,将跳过     expression = e => IgnoreDeleteFilter || !EF.Property<bool>(e, "IsDeleted"); } 

(3)测试:单条件禁用

测试语句如下:

_context.IgnoreDeleteFilter = true; var tenants = _context.Set<TestTenant>().ToList(); 

生成如下SQL:

Executed DbCommand (1ms) [Parameters=[@__ef_filter__IgnoreDeleteFilter_0='?' (DbType = Boolean)], CommandType='Text', CommandTimeout='30'] SELECT [t].[Id], [t].[IsDeleted], [t].[Name], [t].[TenantId] FROM [TestTenant] AS [t] WHERE ((@__ef_filter__IgnoreDeleteFilter_0 = CAST(1 AS bit)) OR ([t].[IsDeleted] = CAST(0 AS bit))) AND ([t].[TenantId] = 1) 

可以看到,原先的软删条件:

([t].[IsDeleted] = CAST(0 AS bit)) 

变成了:

((@__ef_filter__IgnoreDeleteFilter_0 = CAST(1 AS bit)) OR ([t].[IsDeleted] = CAST(0 AS bit))) 

IgnoreDeleteFilter 为 true 时,将会禁用软件的筛选条件。

查询的数据,也确实将软删的数据给查了出来。

4 完整代码

第3节中,完整的代码如下:

Models

namespace EFCoreTest.Models;  public abstract class EntityBase { }  public abstract class EntityBase<TKey> : EntityBase     where TKey : struct {     public TKey Id { get; set; } }  interface ISoftDelete {     public bool IsDeleted { get; set; } }  public class TestDelete : EntityBase<int>, ISoftDelete {     public string? Name { get; set; }     public bool IsDeleted { get; set; } = false; }  public interface ITenant {     public int TenantId { get; set; } }  public class TestTenant : EntityBase<int>, ITenant, ISoftDelete {     public string? Name { get; set; }     public int TenantId { get; set; }     public bool IsDeleted { get; set; } = false; } 

EDbContext

using EFCoreTest.Models; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; using System.Reflection;  namespace EFCoreTest;  public class EDbContext : DbContext {     public bool IgnoreDeleteFilter { get; set; } = false;      public EDbContext(DbContextOptions<EDbContext> options) : base(options) { }      protected override void OnConfiguring(DbContextOptionsBuilder options)     {         base.OnConfiguring(options);     }      protected override void OnModelCreating(ModelBuilder builder)     {         //// 基本注册         //builder.Entity<TestDelete>().HasQueryFilter(e => !e.IsDeleted);          Assembly assembly = typeof(EDbContext).Assembly;         //Assembly assembly = Assembly.GetExecutingAssembly();         List<Type> entityTypes = assembly.GetTypes()             .Where(t => t.IsSubclassOf(typeof(EntityBase)) && !t.IsAbstract)             .ToList();          MethodInfo? configureFilters = typeof(EDbContext).GetMethod(             nameof(ConfigureFilters),             BindingFlags.Instance | BindingFlags.NonPublic         );          if (configureFilters == null) throw new ArgumentNullException(nameof(configureFilters));          foreach(Type entityType in entityTypes)         {             // 注册实体             builder.Entity(entityType);              // 注册筛选器             configureFilters                 .MakeGenericMethod(entityType)                 .Invoke(this, new object[] { builder, entityType });         }     }      protected virtual void ConfigureFilters<TEntity>(ModelBuilder builder, Type entityType)             where TEntity : class     {         Expression<Func<TEntity, bool>>? expression = null;          if (typeof(ISoftDelete).IsAssignableFrom(entityType))         {             expression = e => IgnoreDeleteFilter || !EF.Property<bool>(e, "IsDeleted");         }          if (typeof(ITenant).IsAssignableFrom(entityType))         {             Expression<Func<TEntity, bool>> tenantExpression = e => EF.Property<int>(e, "TenantId") == 1;             expression = expression == null ? tenantExpression : CombineExpressions(expression, tenantExpression);         }          if (expression == null) return;          builder.Entity<TEntity>().HasQueryFilter(expression);     }      protected virtual Expression<Func<T, bool>> CombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)     {         var parameter = Expression.Parameter(typeof(T));          var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter);         var left = leftVisitor.Visit(expression1.Body);          var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);         var right = rightVisitor.Visit(expression2.Body);          return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left, right), parameter);     }      class ReplaceExpressionVisitor : ExpressionVisitor     {         private readonly Expression _oldValue;         private readonly Expression _newValue;          public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)         {             _oldValue = oldValue;             _newValue = newValue;         }          public override Expression Visit(Expression? node)         {             if (node == _oldValue)             {                 return _newValue;             }              return base.Visit(node)!;         }     } } 

测试 Controller

using EFCoreTest; using EFCoreTest.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore;  namespace QueryFilterTest.Controllers;  [ApiController] [Route("[controller]/[action]")] public class TestController : ControllerBase {     private readonly EDbContext _context;     private readonly ILogger<TestController> _logger;      public TestController(ILogger<TestController> logger, EDbContext context)     {         _logger = logger;         _context = context;     }      [HttpGet]     public List<TestDelete> GetDeleteBase()     {         // 过滤 IsDeleted == true 的数据         var deletes = _context.Set<TestDelete>().ToList();          // 忽略筛选器,不过滤数据         var allDeletes = _context.Set<TestDelete>().IgnoreQueryFilters().ToList();          return allDeletes;     }      [HttpGet]     public List<TestTenant> GetTenant()     {         // 软删、多租户 筛选器同时作用         var tenants = _context.Set<TestTenant>().ToList();          // 禁用所有的筛选器         var allTenants = _context.Set<TestTenant>().IgnoreQueryFilters().ToList();          return allTenants;     }      [HttpGet]     public List<TestTenant> GetTenantIgnoreDelete()     {         // 禁用软件筛选器         _context.IgnoreDeleteFilter = true;         var tenants = _context.Set<TestTenant>().ToList();         return tenants;     } } 

完整项目代码:

Gitee:https://gitee.com/lisheng741/testnetcore/tree/master/EFCore/QueryFilterTest

Github:https://github.com/lisheng741/testnetcore/tree/master/EFCore/QueryFilterTest

参考来源

ABP 源码

EF Core 官方文档:全局查询筛选器

EntityFramework Core 2.0全局过滤(HasQueryFilter)

发表评论

相关文章