前言
在项目开发中,我们很多时候都会设计 软删除、所属用户
等等一系列字段 来方便我们在业务查询的时候进行各种过滤
然后引申的问题就是:
在业务查询的时候,我们要如何加上这些条件?或者动态禁用某些查询条件呢?
EF Core自带的全局过滤查询功能
EF Core提供了一个HasQueryFilter
供我们在查询的时候进行预置部分筛选条件
例如:
builder.HasQueryFilter(x => !x.IsDelete);
这样查询的时候 EF Core 会自动帮我们实现过滤
然后如果不想使用的时候可以全部忽略
DbSet.IgnoreQueryFilters();
咋一看 很完美
然后我们实际操作的时候
- 我是不是每个
Entity
里面是不是都要配置一次呢? - 我只想禁用部分筛选条件呢?
- 我的查询条件的某些参数要动态呢?
例如和用户相关的数据等等
(有些人可能会说 我想办法把User
的信息注入到DbContext
里面不就可以了 假如我还要别的信息呢 还是接着注入?)
这就是理论和实践之间的差距
然后再网上找好久,找到了 EntityFramework-Plus
(开源免费)
https://github.com/zzzprojects/EntityFramework-Plus
官网地址: http://entityframework-plus.net/
内置了很多功能 本篇只针对查询过滤做说嘛
EntityFramework-Plus 查询过滤功能
1.QueryFilterManager
QueryFilterManager
主要用来预设全局过滤
例如:
QueryFilterManager.Filter<Customer>(q => q.Where(x => x.IsActive));
var ctx = new EntitiesContext();
QueryFilterManager.InitilizeGlobalFilter(ctx);
这样即可。。。
但是需要提前注意的是 QueryFilterManager
预设后是无法更改的
无法更改这是在 谷歌的时候 作者正好回复的别人的时候看到的
就和我们之前第三点 动态 冲突了
然后只能再看别的方式了
2.Filter
Z.EntityFramework.Plus
提供了 通过DbContext
的扩展方式来进行注入筛选条件的方式
例如:
var ctx = new EntitiesContext();
ctx.Filter<IAnimal>(MyEnum.EnumValue, q => q.Where(x => x.IsDomestic))
//禁用指定键值查询条件
ctx.Filter(MyEnum.EnumValue).Disable();
var dogs = ctx.Dogs.ToList();
//启用指定键值查询条件
ctx.Filter(MyEnum.EnumValue).Enable();
// SELECT * FROM Dog WHERE IsDomestic = true
var dogs = ctx.Dogs.ToList();
这样好像符合我们的需求
3.AsNoFilter
禁用条件
例如:
var ctx = new EntitiesContext();
this.Filter<Customer>(q => q.Where(x => x.IsActive));
// SELECT * FROM Customer WHERE IsActive = true
var list = ctx.Customers.ToList();
// SELECT * FROM Customer
var list = ctx.Customers.AsNoFilter().ToList();
AsNoFilter()
后如何启用 指定查询条件 作者好像没有做相应扩展 ,后面会给出对应扩展方法
说了这么多 理论补完了 实际操作的时候呢?
- 这些条件如何注入进来呢?
- 如何可以让我任意扩展呢?
- 假如我们操作时通过仓储 ,而不是 直接通过DbContext 呢?
如何封装
这边演示通过我自己的开源项目做为事例:
github : https://github.com/wulaiwei/WorkData.Core
主要依赖的框架
- AutoFac
- EF Core
- Z.EntityFramework.Plus
对于我们来说 我们无论使用多少个数据筛选器 返回的都应该是同一个返回值 ,我们去看 DbContext.Filter(....)
会发现他的返回值都是 BaseQueryFilter
针对这个 我们可以得到两条信息 我们需要 传入 DbContext
和 一个返回值为 BaseQueryFilter
的方法
所以 我们定义如下接口 IDynamicFilter
public interface IDynamicFilter
{
BaseQueryFilter InitFilter(DbContext dbContext);
}
这样我们这边就得到了一个标准
例如 我们我们需要一个 所属用户和 软删除 的数据筛选器 我们只需要继承他即可
我们如何区分他们呢?
我们在之前使用 Z.EntityFramework.Plus
是看到了 可以设置筛选器的Key
所以 我们也同样扩展个属性 DynamicFilterAttribute
来作为他们的名字
public class DynamicFilterAttribute: Attribute
{
/// <summary>
/// Name
/// </summary>
public string Name { get; set; }
}
然后我们定义我们的 所属用户和 软删除 的数据筛选器 并为他们设置名称
CreateDynamicFilter
/// <summary>
/// CreateDynamicFilter
/// </summary>
[DynamicFilter(Name = "CreateUserId")]
public class CreateDynamicFilter : IDynamicFilter
{
/// <summary>
/// InitFilter
/// </summary>
/// <param name="dbContext"></param>
/// <returns></returns>
public BaseQueryFilter InitFilter(DbContext dbContext)
{
var workdataSession = IocManager.Instance.Resolve<IWorkDataSession>();
if (workdataSession == null)
return dbContext
.Filter<ICreate>("CreateUserId", x => x.Where(w => w.CreateUserId == string.Empty ));
return dbContext
.Filter<ICreate>("CreateUserId", x => x.Where(w => w.CreateUserId == workdataSession.UserId || w.CreateUserId == ""));
}
}
说明:
var workdataSession = IocManager.Instance.Resolve<IWorkDataSession>();
用来获取你所需要的 传参
IocManager.Instance.Resolve
是WorkData
关于Ioc的封装 源码可以参见git 或者上一篇博客
SoftDeleteDynamicFilter
/// <summary>
/// SoftDeleteDynamicFilter
/// </summary>
[DynamicFilter(Name = "SoftDelete")]
public class SoftDeleteDynamicFilter: IDynamicFilter
{
public BaseQueryFilter InitFilter(DbContext dbContext)
{
return dbContext
.Filter<IsSoftDelete>("SoftDelete", x => x.Where(w => !w.IsDelete));
}
}
这样 我们所有接口 和实现定义好了 如何管理呢?
1. 将继承 IDynamicFilter
的注入到Ioc里面
#region 动态审计注入
var filterTypes = _typeFinder.FindClassesOfType<IDynamicFilter>();
foreach (var filterType in filterTypes)
{
var dynamicFilterAttribute = filterType.GetCustomAttribute(typeof(DynamicFilterAttribute)) as DynamicFilterAttribute;
if (dynamicFilterAttribute == null)
continue;
builder.RegisterType(filterType).Named<IDynamicFilter>(dynamicFilterAttribute.Name);
}
#endregion
说明:
ITypeFinder
是从nopcommerce
抽离出来的反射方法 已集成到WorkData
百度即可查询到相应说明文档- 通过
GetCustomAttribute
获取DynamicFilterAttribute
的属性名称 作为注册到Ioc名称
2.如何设置一个启用数据筛选器呢?我们这边定义个配置文件 通过 .net core 提供的程序进行配置文件注入
/// <summary>
/// 动态拦截器配置
/// </summary>
public class DynamicFilterConfig
{
public List<string> DynamicFilterList{ get; set; }
}
如何注入配置文件 可以通过百度或者查看workdata源码 即可 这不做说明
3.如何管理呢?什么时候统一添加到 DbContext
呢?
我们这边定义一个DynamicFilterManager
提供一个 字典集合 来暂存所以的 IDynamicFilter
,同时提供一个方法来进行初始化值
public static class DynamicFilterManager
{
static DynamicFilterManager()
{
CacheGenericDynamicFilter = new Dictionary<string, IDynamicFilter>();
}
/// <summary>
/// CacheGenericDynamicFilter
/// </summary>
public static Dictionary<string, IDynamicFilter> CacheGenericDynamicFilter { get; set; }
/// <summary>
/// AddDynamicFilter
/// </summary>
/// <param name="dbContext"></param>
/// <returns></returns>
public static void AddDynamicFilter(this DbContext dbContext)
{
if (dbContext == null) return;
foreach (var dynamicFilter in CacheGenericDynamicFilter) dynamicFilter.Value.InitFilter(dbContext);
}
/// <summary>
/// AsWorkDataNoFilter
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="context"></param>
/// <param name="filterStrings"></param>
/// <returns></returns>
public static IQueryable<T> AsWorkDataNoFilter<T>(this DbSet<T> query, DbContext context,
params object[] filterStrings) where T : class
{
var asNoFilterQueryable = query.AsNoFilter();
object query1 = asNoFilterQueryable;
var items = CacheGenericDynamicFilter.Where(x => filterStrings.Contains(x.Key));
query1 = items.Select(key => context.Filter(key.Key)).Where(item => item != null)
.Aggregate(query1, (current, item) => (IQueryable) item.ApplyFilter<T>(current));
return (IQueryable<T>) query1;
}
/// <summary>
/// SetCacheGenericDynamicFilter
/// </summary>
public static void SetCacheGenericDynamicFilter()
{
var dynamicFilterConfig = IocManager.Instance.ResolveServiceValue<DynamicFilterConfig>();
foreach (var item in dynamicFilterConfig.DynamicFilterList)
{
var dynamicFilter = IocManager.Instance.ResolveName<IDynamicFilter>(item);
CacheGenericDynamicFilter.Add(item, dynamicFilter);
}
}
}
然后我们在DbContext
里面的 OnModelCreating
进行初始化
/// <summary>
/// 重写模型创建函数
/// </summary>
/// <param name="modelBuilder"></param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//初始化对象
DynamicFilterManager.SetCacheGenericDynamicFilter();
}
初始化完成后如何将条件付给 DbContext
呢?
在DynamicFilterManager
中我们提供了一个扩展方法 AddDynamicFilter
你可以在你创建 DbContext
的时候调用
/// <summary>
/// AddDynamicFilter
/// </summary>
/// <param name="dbContext"></param>
/// <returns></returns>
public static void AddDynamicFilter(this DbContext dbContext)
{
if (dbContext == null) return;
foreach (var dynamicFilter in CacheGenericDynamicFilter) dynamicFilter.Value.InitFilter(dbContext);
}
在WorkData中 我们则需要在EfContextFactory
进行调用
dbContext = _resolver.Resolve<TDbContext>();
//初始化拦截器
dbContext.AddDynamicFilter();
/// <summary>
/// EfContextFactory
/// </summary>
public class EfContextFactory : IEfContextFactory
{
private readonly IResolver _resolver;
public EfContextFactory(IResolver resolver)
{
_resolver = resolver;
}
/// <summary>
/// default current context
/// </summary>
/// <param name="dic"></param>
/// <param name="tranDic"></param>
/// <returns></returns>
public TDbContext GetCurrentDbContext<TDbContext>(Dictionary<string, DbContext> dic, Dictionary<DbContext, IDbContextTransaction> tranDic)
where TDbContext : DbContext
{
return GetCurrentDbContext<TDbContext>(dic, tranDic, string.Empty);
}
/// <summary>
///GetCurrentDbContext
/// </summary>
/// <typeparam name="TDbContext"></typeparam>
/// <param name="dic"></param>
/// <param name="tranDic"></param>
/// <param name="conString"></param>
/// <returns></returns>
public TDbContext GetCurrentDbContext<TDbContext>(Dictionary<string, DbContext> dic, Dictionary<DbContext, IDbContextTransaction> tranDic, string conString)
where TDbContext : DbContext
{
conString = typeof(TDbContext).ToString();
var dbContext = dic.ContainsKey(conString + "DbContext") ? dic[conString + "DbContext"] : null;
try
{
if (dbContext != null)
{
return (TDbContext)dbContext;
}
}
catch (Exception)
{
dic.Remove(conString + "DbContext");
}
dbContext = _resolver.Resolve<TDbContext>();
//初始化拦截器
dbContext.AddDynamicFilter();
//我们在创建一个,放到数据槽中去
dic.Add(conString + "DbContext", dbContext);
//开始事务
var tran = dbContext.Database.BeginTransaction();
tranDic.Add(dbContext, tran);
return (TDbContext)dbContext;
}
}
这样我们的筛选器已经全部注入完成了
还剩下一个我们之前说的
AsNoFilter()
后如何启用 指定查询条件 作者好像没有做相应扩展 ,后面会给出对应扩展方法
通过查看源码后
/// <summary>
/// Filter the query using context filters associated with specified keys.
/// </summary>
/// <typeparam name="T">The type of elements of the query.</typeparam>
/// <param name="query">The query to filter using context filters associated with specified keys.</param>
/// <param name="keys">
/// A variable-length parameters list containing keys associated to context filters to use to filter the
/// query.
/// </param>
/// <returns>The query filtered using context filters associated with specified keys.</returns>
public static IQueryable<T> Filter<T>(this DbSet<T> query, params object[] keys) where T : class
{
BaseQueryFilterQueryable filterQueryable = QueryFilterManager.GetFilterQueryable((IQueryable) query);
IQueryable<T> query1 = filterQueryable != null ? (IQueryable<T>) filterQueryable.OriginalQuery : (IQueryable<T>) query;
return QueryFilterManager.AddOrGetFilterContext(filterQueryable != null ? filterQueryable.Context : InternalExtensions.GetDbContext<T>(query)).ApplyFilter<T>(query1, keys);
}
Z.EntityFramework.Plus
提供了一个 ApplyFilter
所以 我们基于这个 做个扩展
/// <summary>
/// AsWorkDataNoFilter
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="context"></param>
/// <param name="filterStrings"></param>
/// <returns></returns>
public static IQueryable<T> AsWorkDataNoFilter<T>(this DbSet<T> query, DbContext context,
params object[] filterStrings) where T : class
{
var asNoFilterQueryable = query.AsNoFilter();
object query1 = asNoFilterQueryable;
var items = CacheGenericDynamicFilter.Where(x => filterStrings.Contains(x.Key));
query1 = items.Select(key => context.Filter(key.Key)).Where(item => item != null)
.Aggregate(query1, (current, item) => (IQueryable) item.ApplyFilter<T>(current));
return (IQueryable<T>) query1;
}
这样 我们可以传入指定的筛选器名称 启用自己想要的
最终我们的仓储就变成了这样:
/// <summary>
/// EfBaseRepository
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TPrimaryKey"></typeparam>
/// <typeparam name="TDbContext"></typeparam>
public class EfBaseRepository<TDbContext, TEntity, TPrimaryKey> :
BaseRepository<TEntity, TPrimaryKey>,
IRepositoryDbConntext where TEntity : class, IAggregateRoot, IEntity<TPrimaryKey>
where TDbContext : DbContext
{
//public IQueryable<EntityType> EntityTypes => Context.Model.EntityTypes.Where(t => t.Something == true);
private readonly IDbContextProvider<TDbContext> _dbContextProvider;
private readonly IPredicateGroup<TEntity> _predicateGroup;
public EfBaseRepository(
IDbContextProvider<TDbContext> dbContextProvider,
IPredicateGroup<TEntity> predicateGroup)
{
_dbContextProvider = dbContextProvider;
_predicateGroup = predicateGroup;
}
/// <summary>
/// Gets EF DbContext object.
/// </summary>
public TDbContext Context => _dbContextProvider.GetContent();
/// <summary>
/// Gets DbSet for given entity.
/// </summary>
public virtual DbSet<TEntity> DbSet => Context.Set<TEntity>();
#region DbContext
/// <summary>
/// GetDbContext
/// </summary>
/// <returns></returns>
public DbContext GetDbContext()
{
return Context;
}
#endregion
#region Query
/// <summary>
/// FindBy
/// </summary>
/// <param name="primaryKey"></param>
/// <returns></returns>
public override TEntity FindBy(TPrimaryKey primaryKey)
{
var entity = DbSet.Find(primaryKey);
return entity;
}
/// <summary>
/// FindBy
/// </summary>
/// <param name="primaryKey"></param>
/// <param name="includeNames"></param>
/// <returns></returns>
public override TEntity FindBy(TPrimaryKey primaryKey, string[] includeNames)
{
var query = DbSet;
foreach (var includeName in includeNames)
{
query.Include(includeName);
}
var entity = query.Find(primaryKey);
return entity;
}
/// <summary>
/// AsNoFilterFindBy
/// </summary>
/// <param name="primaryKey"></param>
/// <returns></returns>
public override TEntity AsNoFilterFindBy(TPrimaryKey primaryKey)
{
var entity = DbSet.AsNoFilter()
.SingleOrDefault(x => x.Id.Equals(primaryKey));
return entity;
}
/// <summary>
/// AsNoFilterFindBy
/// </summary>
/// <param name="primaryKey"></param>
/// <param name="includeNames"></param>
/// <returns></returns>
public override TEntity AsNoFilterFindBy(TPrimaryKey primaryKey, string[] includeNames)
{
var query = DbSet.AsNoFilter();
foreach (var includeName in includeNames)
{
query.Include(includeName);
}
var entity = query.SingleOrDefault(x => x.Id.Equals(primaryKey));
return entity;
}
/// <summary>
/// FindBy
/// </summary>
/// <param name="primaryKey"></param>
/// <param name="filterStrings"></param>
/// <returns></returns>
public override TEntity FindBy(TPrimaryKey primaryKey, params object[] filterStrings)
{
var entity = DbSet.AsWorkDataNoFilter(Context, filterStrings)
.SingleOrDefault(x => x.Id.Equals(primaryKey));
return entity;
}
/// <summary>
/// FindBy
/// </summary>
/// <param name="primaryKey"></param>
/// <param name="includeNames"></param>
/// <param name="filterStrings"></param>
/// <returns></returns>
public override TEntity FindBy(TPrimaryKey primaryKey, string[] includeNames, params object[] filterStrings)
{
var query = DbSet.AsWorkDataNoFilter(Context, filterStrings);
foreach (var includeName in includeNames)
{
query.Include(includeName);
}
var entity = query.SingleOrDefault(x => x.Id.Equals(primaryKey));
return entity;
}
/// <summary>
/// GetAll
/// </summary>
/// <returns></returns>
public override IQueryable<TEntity> GetAll()
{
return DbSet;
}
/// <summary>
/// GetAll
/// </summary>
/// <param name="includeNames"></param>
/// <returns></returns>
public override IQueryable<TEntity> GetAll(string[] includeNames)
{
var query = DbSet;
foreach (var includeName in includeNames)
{
query.Include(includeName);
}
return query;
}
/// <summary>
/// GetAll
/// </summary>
/// <param name="filterStrings"></param>
/// <returns></returns>
public override IQueryable<TEntity> GetAll(params object[] filterStrings)
{
return DbSet.AsWorkDataNoFilter(Context, filterStrings);
}
/// <summary>
/// GetAll
/// </summary>
/// <param name="includeNames"></param>
/// <param name="filterStrings"></param>
/// <returns></returns>
public override IQueryable<TEntity> GetAll(string[] includeNames, params object[] filterStrings)
{
var query = DbSet.AsWorkDataNoFilter(Context, filterStrings);
foreach (var includeName in includeNames)
{
query.Include(includeName);
}
return query;
}
/// <summary>
/// AsNoFilterGetAll
/// </summary>
/// <returns></returns>
public override IQueryable<TEntity> AsNoFilterGetAll()
{
return DbSet.AsNoFilter();
}
/// <summary>
/// AsNoFilterGetAll
/// </summary>
/// <param name="includeNames"></param>
/// <returns></returns>
public override IQueryable<TEntity> AsNoFilterGetAll(string[] includeNames)
{
var query = DbSet.AsNoFilter();
foreach (var includeName in includeNames)
{
query.Include(includeName);
}
return query;
}
#endregion
#region Insert
/// <summary>
/// Insert
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="model"></param>
public override TEntity Insert(TEntity model)
{
return DbSet.Add(model).Entity;
}
/// <summary>
/// InsertGetId
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
public override TPrimaryKey InsertGetId(TEntity model)
{
model = Insert(model);
Context.SaveChanges();
return model.Id;
}
/// <summary>
/// Insert
/// </summary>
/// <param name="entities"></param>
public override void Insert(IEnumerable<TEntity> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
DbSet.AddRange(entities);
Context.SaveChanges();
}
#endregion
#region Delete
/// <summary>
/// Delete
/// </summary>
/// <param name="entity"></param>
public override void Delete(TEntity entity)
{
DbSet.Remove(entity);
Context.SaveChanges();
}
/// <summary>
/// Delete
/// </summary>
/// <param name="entities"></param>
public override void Delete(IEnumerable<TEntity> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
DbSet.RemoveRange(entities);
Context.SaveChanges();
}
#endregion
#region Update
/// <summary>
/// Update
/// </summary>
/// <param name="entity"></param>
public override void Update(TEntity entity)
{
DbSet.Update(entity);
Context.SaveChanges();
}
/// <summary>
/// Update
/// </summary>
/// <param name="entities"></param>
public override void Update(IEnumerable<TEntity> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
DbSet.UpdateRange(entities);
Context.SaveChanges();
}
#endregion
}
说明:仓储的设计理念是从 ABP中抽离出来的
最后附测试
启用的筛选器为 “CreateUserId
”, “SoftDelete
”
/// <summary>
/// Index
/// </summary>
/// <returns></returns>
public IActionResult Index()
{
_baseRepository.GetAll().ToList();
_baseRepository.GetAll("CreateUserId","xxx假定不存在的筛选器").ToList();
_baseRepository.AsNoFilterGetAll().ToList();
_baseRepository.FindBy("1");
_baseRepository.FindBy("1", "CreateUserId", "xxx假定不存在的筛选器");
_baseRepository.AsNoFilterFindBy("1");
return View();
}