EF Core 中实现 动态数据过滤器

前言

在项目开发中,我们很多时候都会设计 软删除、所属用户 等等一系列字段 来方便我们在业务查询的时候进行各种过滤

然后引申的问题就是:

在业务查询的时候,我们要如何加上这些条件?或者动态禁用某些查询条件呢?

EF Core自带的全局过滤查询功能

EF Core提供了一个HasQueryFilter 供我们在查询的时候进行预置部分筛选条件

例如:

builder.HasQueryFilter(x => !x.IsDelete);

这样查询的时候 EF Core 会自动帮我们实现过滤
然后如果不想使用的时候可以全部忽略

DbSet.IgnoreQueryFilters();

咋一看 很完美

然后我们实际操作的时候

  1. 我是不是每个Entity里面是不是都要配置一次呢?
  2. 我只想禁用部分筛选条件呢?
  3. 我的查询条件的某些参数要动态呢?

例如和用户相关的数据等等
(有些人可能会说 我想办法把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()后如何启用 指定查询条件 作者好像没有做相应扩展 ,后面会给出对应扩展方法


说了这么多 理论补完了 实际操作的时候呢?

  1. 这些条件如何注入进来呢?
  2. 如何可以让我任意扩展呢?
  3. 假如我们操作时通过仓储 ,而不是 直接通过DbContext 呢?

如何封装

这边演示通过我自己的开源项目做为事例:
github : https://github.com/wulaiwei/WorkData.Core

主要依赖的框架

  1. AutoFac
  2. EF Core
  3. 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.ResolveWorkData 关于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

说明:

  1. ITypeFinder 是从 nopcommerce 抽离出来的反射方法 已集成到WorkData 百度即可查询到相应说明文档
  2. 通过 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();
        }

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值