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;
}
}
二、应用
上一小节我们查找到了继承基类的所有实体,那么现在我们就将全局过滤器应用到实体。
- 第一步 首先,获取租户 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;
}
}
- 第二步 在上下文中定义全局查询过滤的泛型方法:
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);
}
三、总结
这篇文章这是简单的实现了多租户和软删除,队医业务场景更加复杂的项目,我们需要利用一些特殊方法来实现全局查询过滤器。上面所说的方法只是其中一种,不排除由更好的方法。
- 【亲测有效】Win10家庭版Microsoft Edge页面出现乱码的两种解决方案及gpedit.msc命令无法使用的解决策略
- Fiddler抓包7-post请求(json)
- Selenium2+python自动化56-unittest之断言(assert)
- 长文 | 手把手教你如何使用python进行数据分析(最好将文章代码自己码一遍)
- 回归与梯度下降法及实现原理
- 【宅男宅女们的福音】电影天堂最新电影爬取及搜索脚本
- 把模块有关联的放在一个文件夹中 在python2中调用文件夹名会直接失败 在python3中调用会成功,但是调用不能成功的解决方案
- numpy用法小结
- 凯撒密码加解密及破解实现原理
- linux bash Shell脚本经典 Fork炸弹演示及命令详解
- python易错盲点排查之+=与+的区别分析以及一些赋值运算踩过的坑
- Selenium2+python自动化57-捕获异常(NoSuchElementException)
- 你真的会用ABAP, Java和JavaScript里的constructor么?
- 【Python学习笔记之三】lambda表达式用法小结
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法