• 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().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 的代码变为如下:

    修改了 OnModelCreatingConfigureFilters 的大部分代码:

    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 源码

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

    EntityFramework Core 2.0全局过滤(HasQueryFilter)

  • 相关阅读:
    Java HashTable类简介说明
    XFeat:速度精度远超superpoint的轻量级图像匹配算法
    JNI动态注册以及JNI签名
    【Android -- 开发】初级工程师进阶
    leetcode 1523. 在区间范围内统计奇数数目
    无人机技术服务应用
    为什么 ArrayList的 初始容量为10?每次扩容1.5倍?
    ALV细节再梳理2022.8.5
    HTML前端面试基础(一)
    算法金 | 必会的机器学习评估指标
  • 原文地址:https://www.cnblogs.com/clis/p/16501586.html