浅谈NETCore中的默认查询过滤(如软删除)
我们知道,如果在业务界面上删除一条数据,通常的做法是与后台通信,从数据库表中删除掉这一条记录,这种方式通常被称为硬删除。然而这种方式会带来一个弊端,即数据一旦删除了,就真的永久删除了,没有后悔药可以吃,也没有办法恢复。这样,在一些场景中,比如需要保留用户删除的痕迹或能够恢复删除的数据的时候,硬删除就没有办法满足需求了。因此,相对于硬删除,聪明的人们又想到了软删除。
软删除的概念
软删除又叫逻辑删除或标记删除,是一种对立于硬删除的删除方式。这种方式并不是真正的从数据库表中把记录删除,而是通过特定的标记方式标记此记录已被删除(在查询的时候过滤掉此记录),这样用户在界面上看起来就像是数据真的被删掉了,然而事实上却是库里还在,甚至明天还想再见他。
设计软删除的原则与总结
在设计软删除的时候,一定要考虑业务上是否一定需要软删除,如果不需要请不要画蛇添足。
当在业务上需要软删除的情况下,就要开始考虑业务的数据量和读写的比例,从表数据量的角度去分析、优化数据库的性能。
虽然软删除从逻辑上看更加严谨,且能够保证数据的完整性,但这并不代表我们在任何时候都要使用软删除。当我们确定某些数据真的不需要的时候,硬删除是十分必要的,因为这样能减少数据库表中的记录数量,有效提升数据库性能。
=============================================
上面讲完了概念,下面我们开始讲代码怎么来实现,我们以软删除为例,其他举一反三即可。
在代码中实现软删除,我们要尽量做到底层数据库上下文当中,避免业务层去写,很容易忘记的,导致获取的数据不正确。
要查询默认软删除,我们在底层定义一个接口,这个接口在数据库层、业务层都能使用到。
我们定义一个 ISoftDelete 的接口类
public interface ISoftDelete { bool IsDeleted { get; set; } }
需要设置软删除的实体,都继承该接口,如
public class Project:EntityBase<int>,ISoftDelete { [DisplayName("用户Id")] public string UserId { get; set; } [DisplayName("项目名称")] public string TecName { get; set; } [DisplayName("是否删除")] public bool IsDeleted { get; set; } }
EntityBase 是我定义了一个id 的基类。
基础工作准备好后,默认查询过滤主要在数据库上下文中处理。
我们这边介绍2种方式,一种是EF默认提供的,第二种是基于EF我们优化的。
一、EF提供的查询过滤
EF提供了HasQueryFilter 方法来实现默认过滤。
我们可以在定义的数据库上下文OnModelCreating方法中这么写
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<Project>().HasQueryFilter(m => m.IsDeleted); }
这样就对每一个有设置的实体,进行了默认的查询过滤。
那如果设置了之后,我们在一些场景下需要把已删除的数据也查询出来呢?那就需要用到EF提供忽略查询过滤功能,如果设置忽略了,则该实体的查询将全部不再默认过滤了。
如下代码
_context.Set<Project>().IgnoreQueryFilters()
_context为我们的数据库上下文对象,通过设置 Set对象的 IgnoreQueryFilters 方法
该方法返回的是IQueryable 对象。后面要查询直接 .Where() 即可。
但是这种方式有个弊端,就是在OnModelCreating ,我们每个实体都要去写一遍,很繁琐。
下面我们就介绍第二种方式
二、利用反射自动添加DbSet 实体和过滤处理
这个原理也是比较简单,就是对定义实体的库的dll,通过反射,获取里面的实体类。然后遍历,加入到数据库上下文的DbSet中。同时,注册过滤查询。
重点在于代码实现
下面这个方法,是从dll文件中反射,获取实体类的集合。
private List<Assembly> GetModelAssembly() { var list = new List<Assembly>(); //var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package");//排除所有的系统程序集、Nuget下载包 //实体层的寻找有两种方式,1种是配置,多个用英文逗号隔开,1种按照格式寻找,必须 *.Model的固定格式 if (!string.IsNullOrWhiteSpace(ModelNames)) //ModelNames是实体类库的名称,比如Test.Model { string[] modelArr = ModelNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); foreach (var modelName in modelArr) { Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(modelName)); list.Add(assembly); } } else { var deps = DependencyContext.Default; List<CompilationLibrary> libs = deps.CompileLibraries.Where(m => m.Name.Contains(".Model") && !m.Serviceable && m.Type != "package").ToList(); if (libs.Any()) { foreach (var lib in libs) { if (lib != null) { Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name)); list.Add(assembly); } } } } return list; }
下面这个代码是对实体类进行遍历,写入数据库上下文的DbSet和过滤条件
private ModelBuilder RegisteModel(ModelBuilder modelBuilder) { List<Assembly> assemblyList = GetModelAssembly(); if (assemblyList.Any()) { //对过滤特殊处理 MethodInfo configureFilters = typeof(DbContextBase).GetMethod(nameof(ConfigureFilters), BindingFlags.Instance | BindingFlags.NonPublic); //DbContextBase为自定义数据库上下文对象 if (configureFilters == null) throw new ArgumentNullException(nameof(configureFilters)); foreach (var assembly in assemblyList) { foreach (Type type in assembly.ExportedTypes) { if (type.IsClass && type != typeof(EntityBase<>) && !type.IsAbstract ) //默认找继承EntityBase的,我们规定所有实体都继承这个 { if (typeof(ISoftDelete).IsAssignableFrom(type)) //如果实体继承软删除接口,则调用注册过滤的方法 { //增加软删除的过滤 configureFilters.MakeGenericMethod(type).Invoke(this, new object[] { modelBuilder }); } // 防止重复附加模型,否则会在生成指令中报错 if (modelBuilder.Model.FindEntityType(type) != null) continue; // modelBuilder.Model.AddEntityType(type); modelBuilder.Entity(type); //将实体加入到DbSet当中 // modelBuilder.Model.AddEntityType(type); } } } } return modelBuilder; }
/// <summary> /// 自定义配置筛选方法 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="builder"></param> 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);
}
通过以上代码,就实现了自动注册和过滤功能。
同样的,若是要忽略过滤条件,只需要操作dbset对象即可。若在仓储中没有dbset,那需要把dbset定义一个public 对象出来使用。
如:
_projectRepository._dbSet.IgnoreQueryFilters()
代码示例就是在底层Repository中定义了 DbSet类型的_dbSet变量,把这个变量公开出来,就可以操作了
若是有多个默认过滤的,比如租户等,依次写法优化即可。
以上就是简单的,EF底层增加默认过滤的方式了。
推荐参考资料: https://www.sfjvip.com/csharp/8878.html
更多分享,请大家关注我的个人公众号:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)