4

EF Core 数据过滤

 2 years ago
source link: https://www.cnblogs.com/clis/p/16501586.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

EF Core 数据过滤

本文致力于将一种动态数据过滤的方案描述出来(基于 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)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK