Entity Framework Core 实现全局查询过滤

简介: Entity Framework Core 实现全局查询过滤

微软在 Entity Framework Core 2+ 中引入了全局查询过滤器,简化了构建多租户应用程序和实体软删除的复杂度。这篇文章我将通过代码的形式对全局过滤查询进行详细的讲解。在讲解前我们先来简单说一下什么是多租户,所谓多租户简单来说是指一个单独的实例可以为多个组织服务。多租户技术为共用的数据中心内如何以单一系统架构与服务提供多数客户端相同甚至可定制化的服务,并且仍然可以保障客户的数据隔离。


接下来我们先来看一个例子,我们假定多个租户使用同一个数据库,同一个Schema,区分租户是根据表中的 tId 区分。我们新建一个项目,在项目中重写 DbContext 上下文里的 OnModelCreating 方法,在这个方法中我们使用 HasQueryFilter 方法进行软删除。

public class EFContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Employee>().HasQueryFilter(p => !p.IsDelete);
        modelBuilder.Entity<Department>().HasQueryFilter(p => !p.IsDelete);
        base.OnModelCreating(modelBuilder);
    }
}

上面这段代码大部分人看完一定觉得没有问题,但是我要告诉各位的是这段代码有一个很大的问题,就是每次去操作 Employee 或 Department 实体时都会应用 HasQueryFilter , 那么如果由几十个甚至上百个 Model 的时候,如果按照上述的方式来编写代码的话那么我们将会配置 HasQueryFilter 几十次上百次。那么问题来了,着这种情况下我们应该怎样支持多租户,应该怎样实现软删除,以及应该怎样实现模型查询过滤的自动检测。下面我们就来一个问题一个问题的讲解。


零、准备基础代码

我们首先准备 Model 代码,代码很简单:

/// <summary>
/// 实体基类
/// </summary>
public class BaseModel
{
    public int Id { get; set; }
    //租户Id
    public int TId { get; set; }
    //是否删除(软删除)
    public bool IsDelete { get; set; }
}
/// <summary>
/// 员工类
/// </summary>
public class Employee:BaseModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Department Department { get; set; }
}
/// <summary>
/// 部门类
/// </summary>
public class Department:BaseModel
{
    public string DepName { get; set; }
    public int EmployeeId { get; set; }
}

下面我们实现 Employee 和 Department 的模型映射:

public class EmployeeConfiguration : IEntityTypeConfiguration<Employee>
{
    public void Configure(EntityTypeBuilder<Employee> builder)
    {
        builder.ToTable("Employee");
        builder.HasKey(p => p.Id);
        builder.Property(p => p.Name);
        builder.Property(p => p.IsDelete);
        builder.Property(p => p.Age);
        builder.HasQueryFilter(p => !p.IsDelete);
    }
}
public class DepartmentConfiguration : IEntityTypeConfiguration<Department>
{
    public void Configure(EntityTypeBuilder<Department> builder)
    {
        builder.ToTable("Department");
        builder.HasKey(p => p.Id);
        builder.Property(p => p.DepName);
        builder.Property(p => p.IsDelete);
        builder.HasQueryFilter(p => !p.IsDelete);
        builder.HasMany(p => p.Employees).WithOne(e => e.Department).HasForeignKey(k => k.DepartmentId);
    }
}

接下来我们再定义一个多租户Id接口和实现,实现里的返回值是定死的,用来模拟检测与当前请求相关的租户。

public interface ITenantProvider
{
    int GetTId();
}
public class TenantProvider : ITenantProvider
{
    public int GetTId()
    {
        return 1;
    }
}

最后我们定义查询 Department 的接口和实现:

public interface IDepartmentDb
{
    IQueryable<Department> GetDepartments();
}
public class DepartmentDb : IDepartmentDb
{
    private EFContext ef;
    public DepartmentDb(EFContext _ef)
    {
        ef = _ef;
    }
    public IQueryable<Department> GetDepartments()
    {
        var departments = ef.Departments;
        return departments;
    }
}

一、 前提条件

如果要为所有实体配置全局查询过滤器,就必须能够自动检测出实体类型,同时类型检测时必须具有缓存支持。基于这两条我们动手创建获取实体类型的接口和实现。首先利用 DependencyContext 获取运行时程序集,将获得的程序集添加到集合中,然后查找出继承自基类 BaseModel 的程序集,如果查找到了就返回,如果没有查找到就实现全局过滤缓存,代码如下:

public interface IEntityTypeProvider
{
     IEnumerable<Type> GetTypes();
}
public class EntityTypeProvider : IEntityTypeProvider
{
    IList<Type> entityTypeCache;
    public IEnumerable<Type> GetTypes()
    {
        if(entityTypeCache!=null)
        {
            return entityTypeCache.ToList();
        }
        entityTypeCache = (from a in GetReferencingAssemblies()
                           from t in a.DefinedTypes
                           where t.BaseType == typeof(BaseModel)
                           select t.AsType()).ToList();
        return entityTypeCache;
    }
    private IEnumerable<Assembly> GetReferencingAssemblies()
    {
        var assemblies = new List<Assembly>();
        var dependencies = DependencyContext.Default.RuntimeLibraries;
        foreach (var library in dependencies)
        {
            var assembly = Assembly.Load(new AssemblyName(library.Name));
            assemblies.Add(assembly);
        }
        return assemblies;
    }
}

二、应用

上一小节我们查找到了继承基类的所有实体,那么现在我们就将全局过滤器应用到实体。

1. 第一步
首先,获取租户 id 和前面对应的实现,并注入到上下文构造函数中:

public class EFContext : DbContext
{
    public DbSet<Employee> Employees;
    public DbSet<Department> Departments;
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new EmployeeConfiguration());
        modelBuilder.ApplyConfiguration(new DepartmentConfiguration());
        base.OnModelCreating(modelBuilder);
    }
    private int tId;
    private IEntityTypeProvider entityTypeProvider;
    public EFContext(DbContextOptions<EFContext> options,ITenantProvider tenantProvider,IEntityTypeProvider entityTypeProvider):base(options)
    {
        tId = tenantProvider.GetTId();
        this.entityTypeProvider = entityTypeProvider;
    }
}

2. 第二步
在上下文中定义全局查询过滤的泛型方法:

public void GlobalQuery<T> (ModelBuilder builder) where T :BaseModel
{
    builder.Entity<T>().HasQueryFilter(e => e.TId == tId && !e.IsDelete);
}

然后在上下文中获取设置全局查询过滤方法的 MethodInfo 类:

static readonly MethodInfo GlobalQueryMethod = typeof(EFContext).GetMethods(BindingFlags.Public | BindingFlags.Instance).Single(p=>p.IsGenericMethod && p.Name== "GlobalQuery");

最后,在OnModelCreating方法中,通过注入的 entityTypeProvider 获取需要全局查询过滤的类型集合,并进行遍历,调用得到进行查询过滤的方法传入 modelBuilder 参数,从而实现多租户查询过滤。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new EmployeeConfiguration());
    modelBuilder.ApplyConfiguration(new DepartmentConfiguration());
    foreach (var item in entityTypeProvider.GetTypes())
    {
        var method = GlobalQueryMethod.MakeGenericMethod(item);
        method.Invoke(this, new object[] { modelBuilder });
    }
    base.OnModelCreating(modelBuilder);
}

三、总结

这篇文章这是简单的实现了多租户和软删除,队医业务场景更加复杂的项目,我们需要利用一些特殊方法来实现全局查询过滤器。上面所说的方法只是其中一种,不排除由更好的方法。

目录
相关文章
|
XML 存储 数据库连接
Entity Framework学习笔记——edmx文件
上文简单介绍了一下Entity FrameWork,这里说一下EF的核心——edmx文件。 在VisualStudio中建立edmx文件(此例环境为VisualStudio2012)
Entity Framework学习笔记——edmx文件
|
2月前
|
SQL 存储 开发框架
【Entity Framework】你必须了解的之自定义SQL查询
【Entity Framework】你必须了解的之自定义SQL查询
31 0
|
存储 开发框架 .NET
Entity Framework基础01
Entity Framework基础01
193 0
Entity Framework基础01
|
数据库
Entity Framework Core介绍(1)
介绍 Entity Framework (EF) Core 是轻量化、可扩展和跨平台版的常用 Entity Framework 数据访问技术。 EF Core 可用作对象关系映射程序 (O/RM),以便于 .NET 开发人员能够使用 .NET 对象来处理数据库,这样就不必经常编写大部分数据访问代码了。
985 0
|
存储 开发框架 数据可视化
Entity Framework Core 简介
Entity Framework Core 简介
196 0
|
索引
Entity Framework 索引
Entity Framework 索引
218 0
|
数据库 数据库管理
Entity Framework 小知识(一)
Entity Framework 小知识(一)
123 0
|
索引
Entity Framework 小知识(四)
Entity Framework 小知识(四)
133 0
|
SQL .NET 数据库
Entity Framework Core 2.0 入门
该文章比较基础, 不多说废话了, 直接切入正题. 该文分以下几点: 创建Model和数据库 使用Model与数据库交互 查询和保存关联数据 EF Core支持情况 EF Core的数据库Providers: 此外还即将支持CosmosDB和 Oracle.
1663 0
|
数据库 容器
Entity Framework Core(3)-配置DbContext
设计时 DbContext 配置 EF Core 设计时工具如迁移需要能够发现和创建的工作实例DbContext以收集有关应用程序的实体类型以及它们如何映射到数据库架构的详细信息的类型。 此过程可以为自动,只要该工具可以轻松地创建DbContext,会将其配置同样到它如何将配置在运行时的方式。
932 0