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 的大部分代码:

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 时,将会禁用软件的筛选条件。

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

参考来源

ABP 源码

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

EntityFramework Core 2.0全局过滤(HasQueryFilter)

EF Core 数据过滤的更多相关文章

  1. [EF Core]数据迁移(二)

    摘要 在实际项目中,大多都需要对业务逻辑以及操作数据库的逻辑进行分成操作,这个时候该如何进行数据的迁移呢? 步骤 上篇文章:EF Core数据迁移操作 比如,我们将数据上下文放在了Data层. 看一下 ...

  2. EF Core 数据变更自动审计设计

    EF Core 数据变更自动审计设计 Intro 有的时候我们需要知道每个数据表的变更记录以便做一些数据审计,数据恢复以及数据同步等之类的事情, EF 自带了对象追踪,使得我们可以很方便的做一些审计工 ...

  3. ef core 全局过滤

    有些固定的条件,基本每个查询的时候需要带的条件,我们可以使用全局过滤来帮我们,这样后面的查询就不用每次都带条件了. 微软自带的:https://docs.microsoft.com/zh-cn/ef/ ...

  4. ef core数据迁移的一点小感悟

    ef core在针对mysql数据迁移的时候,有些时候没法迁移...有两种情况没法迁移,一种是因为efcore的bug问题导致没法迁移,这个在github上有个问题集,另外一种是对数据表进行较大幅度的 ...

  5. EF Core数据访问入门

    重要概念 Entity Framework (EF) Core 是轻量化.可扩展.开源和跨平台的数据访问技术,它还是一 种对象关系映射器 (ORM),它使 .NET 开发人员能够使用面向对象的思想处理 ...

  6. EF Core数据迁移操作

    摘要 在开发中,使用EF code first方式开发,那么如果涉及到数据表的变更,该如何做呢?当然如果是新项目,删除数据库,然后重新生成就行了,那么如果是线上的项目,数据库中已经有数据了,那么删除数 ...

  7. 在ASP.NET Core中通过EF Core实现一个简单的全局过滤查询

    前言 不知道大家是否和我有同样的问题: 一般在数据库的设计阶段,会制定一些默认的规则,其中有一条硬性规定就是一定不要对任何表中的数据执行delete硬删除操作,因为每条数据对我们来说都是有用的,并且是 ...

  8. asp.net core系列 33 EF查询数据 (2)

    一. 原生SQL查询 接着上篇讲.通过 Entity Framework Core 可以在使用关系数据库时下降到原始 SQL 查询. 在无法使用 LINQ 表达要执行的查询时,或因使用 LINQ 查询 ...

  9. 文章翻译:ABP如何在EF core中添加数据过滤器

    原文地址:https://aspnetboilerplate.com/Pages/Documents/Articles%5CHow-To%5Cadd-custom-data-filter-ef-cor ...

随机推荐

  1. 1.9 初学者应选择哪个Linux发行版?

    前面章节中,已经对几个常见的 Linux 发行版做了简单的介绍,那么对于初学者来说,选择哪个发行版的性价比更高呢? 通常情况下,初学者学习 Linux,是为了找一份和 Linux 相关的工作,那么问题 ...

  2. SpringBoot从0到0.7——第一天

    SpringBoot从0到0.7--第一天 学习的第一步当然是收拾好心情,先把环境搭建起来,写出第一个helloword出来. 第一步:安装IDEA和Tomcat 我安装的是IDEA 2021.2.2 ...

  3. 一个登录点两个逻辑漏洞-edusrc

    最近呢, 也是基础漏洞学的差不多了, 就在edusrc上面实战, 刚开始搞一些信息泄漏啥的, 提交了十几个, 结果就他娘的通过了一个. 咱也就不碰信息泄漏了, 没得意思. 关于这个学校测试时也是有坑的 ...

  4. 现代 CSS 解决方案:CSS 数学函数

    在 CSS 中,其实存在各种各样的函数.具体分为: Transform functions Math functions Filter functions Color functions Image ...

  5. HMS Core使能AI智慧体验,共建创新应用生态

    5月17日,2022年搜狐科技峰会成功举办,峰会汇聚各界大咖,共同探讨AI 技术的深入应用以及行业数字化的发展趋势.华为终端云服务应用生态BU总裁望岳发表题为<使能AI智慧体验,共建创新应用生态 ...

  6. 好客租房45-react组件基础综合案例-6边界问题

    边界问题 //导入react import React from 'react' import ReactDOM from 'react-dom' //导入组件 // 约定1:类组件必须以大写字母开头 ...

  7. 【NodeJS】替换模糊查询字符里包含的正则关键字

    问题:正则匹配时字符串中包含了一些特殊字符,导致查询失败 例如,下面的字符包含了( 和 ),这在正则中属于特殊字符 (-)-magnocurarine 正则中的特殊字符如下图 思路: 1.映射查询字符 ...

  8. 分享一款自带工作流引擎的NodeJS全栈框架,接单快手、创业神器

    CabloyJS是什么 CabloyJS是一款自带工作流引擎的Node.js全栈框架, 接单快手.创业神器, 基于koa + egg + vue + framework7 + mysql 在线演示 场 ...

  9. vscode远程调试c++

    0.背景 最近在学习linux webserver开发,需要在linux下调试自己的C/C++代码,但是linux下不像在windows下,直接Visio Studio或者其它集成开发环境那么方便,现 ...

  10. Tensor的向量化

    向量化操作是指可以在同一时间进行批量地并行计算,例如矩阵运算,以达到更好效率的一种方式. 尽量使用向量化直接对Tensor操作,避免低效率的for循环对元素逐个操作.