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().HasQueryFilter(e => !e.IsDeleted);
}
(3)查询中使用
直接查询:
var deletes = _context.Set().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()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();
(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 entityTypes = assembly.GetTypes()
.Where(t => t.IsSubclassOf(typeof(EntityBase)) && !t.IsAbstract)
.ToList();
// 注册实体
foreach(Type entityType in entityTypes)
{
builder.Entity(entityType);
}
3.3 自定注册筛选器
完成这个功能以后,将不需要自己一个一个去注册一些筛选器,如:不需要重复以下这句代码:
builder.Entity().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 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
{
Expressionbool>> expression = e => !EF.Property<bool>(e, "IsDeleted");
builder.Entity().HasQueryFilter(expression);
}
折叠
(3) 测试:自动注册功能的测试
完成自动注册以后,运行程序,看看过滤器是否有效果:
直接查询:
var deletes = _context.Set().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>(Expressionbool >> expression1, Expressionbool >> 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.Lambdabool>>(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 的代码变为如下:
修改了 OnModelCreating
和 ConfigureFilters
的大部分代码:
protected override void OnModelCreating(ModelBuilder builder)
{
Assembly assembly = typeof(EDbContext).Assembly;
List 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
{
Expressionbool>>? expression = null;
if (typeof(ISoftDelete).IsAssignableFrom(entityType))
{
expression = e => !EF.Property<bool>(e, "IsDeleted");
}
if (typeof(ITenant).IsAssignableFrom(entityType))
{
Expressionbool>> tenantExpression = e => EF.Property<int>(e, "TenantId") == 1;
expression = expression == null ? tenantExpression : CombineExpressions(expression, tenantExpression);
}
if (expression == null) return;
builder.Entity().HasQueryFilter(expression);
}
折叠
(4)测试:多条件过滤
直接查询:
var tenants = _context.Set().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().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 options ) : base(options) { }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
base.OnConfiguring(options);
}
protected override void OnModelCreating(ModelBuilder builder)
{
//// 基本注册
//builder.Entity().HasQueryFilter(e => !e.IsDeleted);
Assembly assembly = typeof(EDbContext).Assembly;
//Assembly assembly = Assembly.GetExecutingAssembly();
List 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
{
Expressionbool>>? expression = null;
if (typeof(ISoftDelete).IsAssignableFrom(entityType))
{
expression = e => IgnoreDeleteFilter || !EF.Property<bool>(e, "IsDeleted");
}
if (typeof(ITenant).IsAssignableFrom(entityType))
{
Expressionbool>> tenantExpression = e => EF.Property<int>(e, "TenantId") == 1;
expression = expression == null ? tenantExpression : CombineExpressions(expression, tenantExpression);
}
if (expression == null) return;
builder.Entity().HasQueryFilter(expression);
}
protected virtual Expression<Func<T, bool>> CombineExpressions<T>(Expressionbool >> expression1, Expressionbool >> 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.Lambdabool>>(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 _logger;
public TestController(ILogger logger, EDbContext context )
{
_logger = logger;
_context = context;
}
[HttpGet]
public List GetDeleteBase()
{
// 过滤 IsDeleted == true 的数据
var deletes = _context.Set().ToList();
// 忽略筛选器,不过滤数据
var allDeletes = _context.Set().IgnoreQueryFilters().ToList();
return allDeletes;
}
[HttpGet]
public List GetTenant()
{
// 软删、多租户 筛选器同时作用
var tenants = _context.Set().ToList();
// 禁用所有的筛选器
var allTenants = _context.Set().IgnoreQueryFilters().ToList();
return allTenants;
}
[HttpGet]
public List GetTenantIgnoreDelete()
{
// 禁用软件筛选器
_context.IgnoreDeleteFilter = true;
var tenants = _context.Set().ToList();
return tenants;
}
}
折叠
完整项目代码:
Gitee:https://gitee.com/lisheng741/testnetcore/tree/master/EFCore/QueryFilterTest
Github:https://github.com/lisheng741/testnetcore/tree/master/EFCore/QueryFilterTest
参考来源
ABP 源码