如安在 .NET 中构建一个好用的动态查询天生器

[复制链接]
发表于 2025-5-30 14:35:36 | 显示全部楼层 |阅读模式
媒介

自从.NET Framework 3.5提供了LINQ之后,集合数据查询根本被LINQ统一了。这大幅提高了编写数据查询代码的效率和质量,但是在必要编写动态查询的时候反而很困难,特殊是最常用的where和order by子句,他们的参数是Expression。编写静态查询的时候编译器会主动把代码转换成等价的表达式,而动态查询无法借助编译器完成表达式构建,只能手动拼接。想要正确拼接一个形貌低级代码结构的表达式对开发者的功力提出了较高的要求,哪怕是这方面的高手也容易翻车。
为了简化查询表达式的动态构建,社区出现了很多表达式天生辅助库。其中最知名当属System.Linq.Dynamic.Core和LinqKit。System.Linq.Dynamic.Core利用字符串定义表达式,并在内部转换成Expression,LinqKit则是利用PredicateBuilder把复杂表达式拆分成多个片断的组合。但是他们也存在一些不便之处,System.Linq.Dynamic.Core牺牲了代码的静态检查能力,只有在运行时才知道表达式是否正确。如果把表达式作为允许前端填写的参数,不但必要让前端开发人员多学习一套表达式定义语法,还会产生安全毛病。如果想提前检查表达式的安全性,就必要对字符串进行分析。分析字符串天生表达式会成为一个流行库的原因之一就是分析这个字符串很难,这样一来相当于把外包外包出去的困难任务又拿回来了。LinqKit则是对前端不友爱,这种类型无法序列化传输,如果想通过前端共同利用,还是必要再想办法写一套转换代码和配套的可序列化数据结构。
这两个库在传输序列化和动态拼接简化方面各有显著优势,也各有显着不足。因此笔者开始思考是否有办法开发一个便于序列化传输,安全性能得到静态检查保证,对于复杂表达式的拼接也能良好支持的表达式天生器。经过多次摸索,终有一些心得,在此分享给大家。
新书宣传

有关新书的更多介绍接待查看《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!

虽然本书是基于.NET 6编写的,但是其中大多数内容依然可用,仍旧具有一定的参考代价。
正文

提炼根本概念

在安全的前提下提高灵活性

想要保证构建的查询安全性,势必要限制能够查询的属性,最好能让天生器中只出现可以查询的属性。为了避免底层属性改名导致查询出错,或是隐藏代码中的属性名,袒露给天生器的名称应该和真实属性名解耦,两者能独立调解。对于查询器中可能出现的特殊自定义条件提供自定义扩展点。最好支持静态编译检查和基于主动重构的主动代码调解。
  1. /// <summary>
  2. /// 查询条件构造器接口
  3. /// </summary>
  4. public interface IFilterPredicateBuilder<T>
  5. {
  6.     /// <summary>
  7.     /// 获取查询条件
  8.     /// </summary>
  9.     /// <returns>生成的查询条件</returns>
  10.     Expression<Func<T, bool>>? GetWherePredicate();
  11. }
复制代码
基于以上假设,笔者提炼出了这个根本接口,用于天生器表示支持天生谓词表达式。接口没有任何额外内容以允许最大程度的自定义扩展。
为复杂的嵌套查询提供支持

一个完备的表达式天生一定会面对嵌套对象属性的环境,这其中的问题在于,对象类型无穷无尽,雷同类型的对象也可能出现在各种地方。怎样访问到必要的对象属性并应用筛选条件就是一个必要仔细考虑的问题。在笔者看来,这个问题可以分解为两个子问题,访问属性和应用条件。将这两个部分分离开,条件就可以只针对最终类型开发,属性的访问则交由外部决定。这样一来,针对某种类型开发的条件就可以在任何地方的属性上利用。
  1. /// <summary>
  2. /// 可组合的查询条件构造器接口
  3. /// </summary>
  4. public interface IComposableFilterPredicateBuilder<T>
  5. {
  6.     /// <summary>
  7.     /// 获取查询条件,并把条件应用到<typeparamref name="TOwner"/>类型的对象所拥有的<typeparamref name="T"/>类型的成员上。
  8.     /// </summary>
  9.     /// <typeparam name="TOwner">拥有<typeparamref name="T"/>类型的成员的类型</typeparam>
  10.     /// <param name="memberAccesser">成员访问器</param>
  11.     /// <returns>已应用到成员的查询条件</returns>
  12.     Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccesser);
  13. }
  14. /// <summary>
  15. /// 值类型可组合的查询条件构造器接口
  16. /// </summary>
  17. public interface IStructComposableFilterPredicateBuilder<T> : IComposableFilterPredicateBuilder<T>
  18.     where T : struct
  19. {
  20.     /// <summary>
  21.     /// 获取查询条件,并把条件应用到<typeparamref name="TOwner"/>类型的对象所拥有的<typeparamref name="T"/>类型的成员上。
  22.     /// </summary>
  23.     /// <typeparam name="TOwner">拥有<typeparamref name="T"/>类型的成员的类型</typeparam>
  24.     /// <param name="memberAccesser">成员访问器</param>
  25.     /// <returns>已应用到成员的查询条件</returns>
  26.     Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T?>> memberAccesser);
  27. }
复制代码
基于以上假设,可以再次提炼出一个接口。通过参数由外部决定属性怎样访问,并返回最终拼合条件。值类型必要特殊处理。
为集合类型的查询提供支持

偶然要查询的属性可能是集合类型,这种查询和平凡的单值查询有区别,必要单独处理。
  1. /// <summary>
  2. /// 集合可组合的查询条件构造器接口
  3. /// </summary>
  4. public interface ICollectionComposableFilterPredicateBuilder<T>
  5. {
  6.     /// <summary>
  7.     /// 获取查询条件,并把条件应用到<typeparamref name="TOwner"/>类型的对象所拥有的<typeparamref name="T"/>类型的集合的成员上。
  8.     /// </summary>
  9.     /// <typeparam name="TOwner">拥有<typeparamref name="T"/>类型的集合的成员的类型</typeparam>
  10.     /// <param name="memberAccesser">成员访问器</param>
  11.     /// <returns>已应用到成员的查询条件</returns>
  12.     Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, IEnumerable<T>>> memberAccesser);
  13. }
复制代码
这表示专门用于集合类型的查询,IQueryable实现了IEnumerable,不必要单独定义。
条件反转

一键支持条件反转是个非常有效的功能,如果一个条件有多个子条件,且条件之间混淆了各种加了括号的且或非连接,想要正确反转这样的条件非常困难。
  1. /// <summary>
  2. /// 可反转条件接口
  3. /// </summary>
  4. public interface IPredicateReversible
  5. {
  6.     /// <summary>
  7.     /// 是否反转条件
  8.     /// </summary>
  9.     bool Reverse { get; }
  10. }
复制代码
利用一个bool标记反转条件,由查询天生器主动处理反转是公道的选择。
序列化传输支持

到此为止,一个完备的表达式天生器所需的根本接口就提炼完成了。但是这些接口所表达的概念并不支持序列化传输,接下来就要解决这问题。
序列化传输查询条件意味着要分离出条件中可以序列化的部分。比方:Foo.Bar > 1,此处必要传输的部分是属性,比较方式,比较参数。属性的话由于必要支持静态检查,必要单独处理。至于比较方式,办法比较多,笔者选择利用枚举来表达。关键字一样平常是各种基础类型,应该自然支持序列化。
  1. /// <summary>
  2. /// 引用类型搜索关键字接口
  3. /// </summary>
  4. /// <typeparam name="T">关键字类型</typeparam>
  5. public interface ISearchFilterClassKey<T> where T : class
  6. {
  7.     /// <summary>
  8.     /// 搜索关键字
  9.     /// </summary>
  10.     ImmutableList<T?> Keys { get; }
  11. }
  12. /// <summary>
  13. /// 值类型搜索关键字接口
  14. /// </summary>
  15. /// <typeparam name="T">关键字类型</typeparam>
  16. public interface ISearchFilterStructKey<T> where T : struct
  17. {
  18.     /// <summary>
  19.     /// 搜索关键字
  20.     /// </summary>
  21.     ImmutableList<T?> Keys { get; }
  22. }
  23. /// <summary>
  24. /// 搜索操作符接口
  25. /// </summary>
  26. /// <typeparam name="TOperator"></typeparam>
  27. public interface ISearchFilterOperator<TOperator> where TOperator : struct, Enum
  28. {
  29.     /// <summary>
  30.     /// 搜索操作符
  31.     /// </summary>
  32.     TOperator Operator { get; }
  33. }
复制代码
基于以上假设,可以提炼出以上接口。
为概念接口提供实现

这些接口表达了查询天生器所需的各种概念,但是让开发者自行实现并不是好主意,这些接口对于开发者来说应该是用做泛型束缚的。笔者势必要为此提供一套最常见情形的实现。
  1. /// <summary>
  2. /// 查询构造器基类
  3. /// </summary>
  4. /// <typeparam name="T">查询的数据类型</typeparam>
  5. /// <param name="CombineType">条件谓词组合方式。Json属性名用 combine 减少字数。</param>
  6. /// <inheritdoc cref="IFilterPredicateBuilder{T}"/>
  7. public abstract record QueryBuilderBase<T>(
  8.     [EnumDataType(typeof(PredicateCombineKind))]
  9.     PredicateCombineKind? CombineType = PredicateCombineKind.And)
  10.     : IFilterPredicateBuilder<T>, IComposableFilterPredicateBuilder<T>
  11. {
  12.     /// <inheritdoc/>
  13.     public Expression<Func<T, bool>>? GetWherePredicate()
  14.     {
  15.         var where = BuildWherePredicate();
  16.         if (this is IPredicateReversible reversible) where = reversible.ApplyReversiblePredicate(where);
  17.         return where;
  18.     }
  19.     /// <summary>
  20.     /// 构造查询条件
  21.     /// </summary>
  22.     /// <returns>获得的查询条件</returns>
  23.     /// <remarks>
  24.     /// 派生类重写时请只负责构造自身的条件,
  25.     /// 最后使用<see cref="CombinePredicates"/>合并来自基类的条件后再返回。
  26.     /// 不要在这里进行条件反转。
  27.     /// </remarks>
  28.     protected virtual Expression<Func<T, bool>>? BuildWherePredicate()
  29.     {
  30.         return null;
  31.     }
  32.     /// <summary>
  33.     /// 组合查询条件
  34.     /// </summary>
  35.     /// <param name="predicates">待组合的子条件</param>
  36.     /// <returns></returns>
  37.     /// <exception cref="NotSupportedException"></exception>
  38.     protected Expression<Func<T, bool>>? CombinePredicates(IEnumerable<Expression<Func<T, bool>>>? predicates)
  39.     {
  40.         var predicate = predicates?.FirstOrDefault();
  41.         if (predicates?.Any() is true)
  42.         {
  43.             predicate = CombineType switch
  44.             {
  45.                 PredicateCombineKind.And => predicates?.AndAlsoAll(),
  46.                 PredicateCombineKind.Or => predicates?.OrElseAll(),
  47.                 _ => throw new NotSupportedException(CombineType.ToString()),
  48.             };
  49.         }
  50.         return predicate;
  51.     }
  52.     /// <inheritdoc/>
  53.     /// <exception cref="ArgumentNullException"></exception>
  54.     public Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccesser)
  55.     {
  56.         ArgumentNullException.ThrowIfNull(memberAccesser);
  57.         var where = GetWherePredicate();
  58.         if (where is null) return null;
  59.         MemberReplaceExpressionVisitor visitor = new();
  60.         var result = visitor.ReplaceMember(memberAccesser, where);
  61.         return result;
  62.     }
  63.     /// <summary>
  64.     /// 成员访问表达式替换访问器
  65.     /// </summary>
  66.     private sealed class MemberReplaceExpressionVisitor : ExpressionVisitor
  67.     {
  68.         private readonly Lock _lock = new();
  69.         private volatile bool _calledFromReplaceMember = false;
  70.         private LambdaExpression? _memberAccesser;
  71.         /// <summary>
  72.         /// 把指定表达式的成员访问替换为新的成员访问。
  73.         /// </summary>
  74.         /// <typeparam name="TOwner">拥有<typeparamref name="TMember"/>类型的成员的类型</typeparam>
  75.         /// <typeparam name="TMember">用于替换成员访问的类型</typeparam>
  76.         /// <typeparam name="TResult">返回值类型</typeparam>
  77.         /// <param name="memberAccessor">替换用的新成员访问表达式。</param>
  78.         /// <param name="resultAccessor">要替换成员访问的表达式。</param>
  79.         /// <returns>已替换成员访问的表达式。</returns>
  80.         /// <exception cref="ArgumentNullException"></exception>
  81.         public Expression<Func<TOwner, TResult>> ReplaceMember<TOwner, TMember, TResult>(
  82.             Expression<Func<TOwner, TMember>> memberAccessor,
  83.             Expression<Func<TMember, TResult>> resultAccessor)
  84.         {
  85.             ArgumentNullException.ThrowIfNull(resultAccessor);
  86.             ArgumentNullException.ThrowIfNull(memberAccessor);
  87.             lock (_lock)
  88.             {
  89.                 try
  90.                 {
  91.                     _calledFromReplaceMember = true;
  92.                     _memberAccesser = memberAccessor;
  93.                     var newLambda = (LambdaExpression)Visit(resultAccessor);
  94.                     return Expression.Lambda<Func<TOwner, TResult>>(newLambda.Body, memberAccessor.Parameters);
  95.                 }
  96.                 catch
  97.                 {
  98.                     throw;
  99.                 }
  100.                 finally
  101.                 {
  102.                     _calledFromReplaceMember = false;
  103.                     _memberAccesser = null;
  104.                 }
  105.             }
  106.         }
  107.         /// <inheritdoc/>
  108.         [return: NotNullIfNotNull(nameof(node))]
  109.         public override Expression? Visit(Expression? node)
  110.         {
  111.             if (!_calledFromReplaceMember) throw new InvalidOperationException($"Don't call directly, call {nameof(ReplaceMember)} instead.");
  112.             return base.Visit(node);
  113.         }
  114.         /// <inheritdoc/>
  115.         protected override Expression VisitMember(MemberExpression node)
  116.         {
  117.             if (node.Expression is ParameterExpression)
  118.             {
  119.                 return Expression.PropertyOrField(_memberAccesser!.Body, node.Member.Name);
  120.             }
  121.             return base.VisitMember(node);
  122.         }
  123.     }
  124. }
  125. /// <summary>
  126. /// 条件谓词组合方式
  127. /// </summary>
  128. public enum PredicateCombineKind
  129. {
  130.     /// <summary>
  131.     /// 且
  132.     /// </summary>
  133.     And = 1,
  134.     /// <summary>
  135.     /// 或
  136.     /// </summary>
  137.     Or = 2
  138. }
  139. /// <summary>
  140. /// 可反转谓词接口扩展
  141. /// </summary>
  142. public static class PredicateReversibleExtensions
  143. {
  144.     /// <summary>
  145.     /// 应用可反转的谓词
  146.     /// </summary>
  147.     /// <typeparam name="T">谓词表达式的参数类型</typeparam>
  148.     /// <param name="reversible">可反转谓词接口的实例</param>
  149.     /// <param name="predicate">谓词表达式</param>
  150.     /// <returns></returns>
  151.     public static Expression<Func<T, bool>>? ApplyReversiblePredicate<T>(
  152.         this IPredicateReversible reversible,
  153.         Expression<Func<T, bool>>? predicate)
  154.     {
  155.         return !reversible.Reverse ? predicate : predicate?.Not();
  156.     }
  157. }
复制代码
在接口的GetWherePredicate()方法之外,增加一个内部的BuildWherePredicate()方法,把天生根本条件和反转条件隔离开并统一处理,确保反转条件只会在末了进行一次。
实现通用的根本类型过滤器表达式

定义操纵类型
  1. /// <summary>
  2. /// 基本搜索操作
  3. /// </summary>
  4. public enum BaseSearchOperator : uint
  5. {
  6.     /// <summary>
  7.     /// 等于
  8.     /// </summary>
  9.     Equal = 1 << 0,
  10.     /// <summary>
  11.     /// 是候选项之一
  12.     /// </summary>
  13.     In = 1 << 1,
  14. }
  15. /// <summary>
  16. /// 字符串搜索操作
  17. /// </summary>
  18. public enum StringSearchOperator : uint
  19. {
  20.     /// <summary>
  21.     /// 等于
  22.     /// </summary>
  23.     Equal = 1 << 0,
  24.     /// <summary>
  25.     /// 是候选项之一
  26.     /// </summary>
  27.     In = 1 << 1,
  28.     /// <summary>
  29.     /// 包含
  30.     /// </summary>
  31.     Contains = 1 << 2,
  32.     /// <summary>
  33.     /// 包含全部候选项
  34.     /// </summary>
  35.     EqualContains = Equal | Contains,
  36.     /// <summary>
  37.     /// 包含候选项之一
  38.     /// </summary>
  39.     InContains = In | Contains,
  40.     /// <summary>
  41.     /// 开头是
  42.     /// </summary>
  43.     StartsWith = 1 << 3,
  44.     /// <summary>
  45.     /// 开头是候选项之一
  46.     /// </summary>
  47.     InStartsWith = In | StartsWith,
  48.     /// <summary>
  49.     /// 结尾是
  50.     /// </summary>
  51.     EndsWith = 1 << 4,
  52.     /// <summary>
  53.     /// 结尾是候选项之一
  54.     /// </summary>
  55.     InEndsWith = In | EndsWith,
  56. }
  57. /// <summary>
  58. /// 可排序数字搜索操作
  59. /// </summary>
  60. public enum ComparableNumberSearchOperator : uint
  61. {
  62.     /// <summary>
  63.     /// 等于
  64.     /// </summary>
  65.     Equal = 1 << 0,
  66.     /// <summary>
  67.     /// 是候选项之一
  68.     /// </summary>
  69.     In = 1 << 1,
  70.     /// <summary>
  71.     /// 小于
  72.     /// </summary>
  73.     LessThan = 1 << 2,
  74.     /// <summary>
  75.     /// 小于等于
  76.     /// </summary>
  77.     LessThanOrEqual = LessThan | Equal,
  78.     /// <summary>
  79.     /// 大于
  80.     /// </summary>
  81.     GreaterThan = 1 << 3,
  82.     /// <summary>
  83.     /// 大于等于
  84.     /// </summary>
  85.     GreaterThanOrEqual = GreaterThan | Equal,
  86.     /// <summary>
  87.     /// 介于两个值之间,但不包含两边的边界值
  88.     /// </summary>
  89.     BetweenOpen = 1 << 4,
  90.     /// <summary>
  91.     /// 是多组介于两个值之间,但不包含两边的边界值的候选区间之一
  92.     /// </summary>
  93.     InBetweenOpen = In | BetweenOpen,
  94.     /// <summary>
  95.     /// 介于两个值之间,包含左边界值,但不包含右边界值
  96.     /// </summary>
  97.     BetweenLeftClosed = 1 << 5,
  98.     /// <summary>
  99.     /// 是多组介于两个值之间,包含左边界值,但不包含右边界值的候选区间之一
  100.     /// </summary>
  101.     InBetweenLeftClosed = In | BetweenLeftClosed,
  102.     /// <summary>
  103.     /// 介于两个值之间,包含右边界值,但不包含左边界值
  104.     /// </summary>
  105.     BetweenRightClosed = 1 << 6,
  106.     /// <summary>
  107.     /// 是多组介于两个值之间,包含右边界值,但不包含左边界值的候选区间之一
  108.     /// </summary>
  109.     InBetweenRightClosed = In | BetweenRightClosed,
  110.     /// <summary>
  111.     /// 介于两个值之间,同时包含两边的边界值
  112.     /// </summary>
  113.     BetweenClosed = BetweenOpen | BetweenLeftClosed | BetweenRightClosed,
  114.     /// <summary>
  115.     /// 是多组介于两个值之间,同时包含两边的边界值的候选区间之一
  116.     /// </summary>
  117.     InBetweenClosed = In | BetweenClosed,
  118. }
复制代码
此处展示了一部分基础数据类型的过滤器定义。如果将来有自定义根本类型也可以照葫芦画瓢。
集合类型属性的实现
  1. /// <summary>
  2. /// 值类型基本搜索过滤器
  3. /// </summary>
  4. /// <typeparam name="T">要搜索的值类型</typeparam>
  5. public record StructSearchFilter<T>
  6.     : ISearchFilterStructKey<T>
  7.     , ISearchFilterOperator<BaseSearchOperator>
  8.     , IStructComposableFilterPredicateBuilder<T>
  9.     , IPredicateReversible
  10.     where T : struct
  11. {
  12.     private static readonly Type _baseType = typeof(T);
  13.     private static readonly Type _nullableType = typeof(T?);
  14.     private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
  15.         .GetMethods()
  16.         .Where(static m => m.Name is nameof(Enumerable.Contains))
  17.         .Single(static m => m.GetParameters().Length is 2)
  18.         .MakeGenericMethod([_baseType]);
  19.     private static readonly MethodInfo _enumerableNullableContains = typeof(Enumerable)
  20.         .GetMethods()
  21.         .Where(static m => m.Name is nameof(Enumerable.Contains))
  22.         .Single(static m => m.GetParameters().Length is 2)
  23.         .MakeGenericMethod([_nullableType]);
  24.     /// <summary>
  25.     /// 初始化一个新实例
  26.     /// </summary>
  27.     /// <param name="keys">搜索关键字</param>
  28.     /// <param name="operator">搜索操作符</param>
  29.     /// <param name="reverse">是否反转条件</param>
  30.     /// <exception cref="ArgumentException"></exception>
  31.     /// <exception cref="InvalidEnumArgumentException"></exception>
  32.     public StructSearchFilter(
  33.         ImmutableList<T?> keys,
  34.         [EnumDataType(typeof(BaseSearchOperator))]
  35.         BaseSearchOperator @operator = BaseSearchOperator.Equal,
  36.         bool reverse = false)
  37.     {
  38.         ArgumentNullException.ThrowIfNull(nameof(keys));
  39.         if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
  40.         if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());
  41.         if (@operator is BaseSearchOperator.In && keys is null or { Count: < 2 })
  42.         {
  43.             throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时必须设置多个元素。", nameof(keys));
  44.         }
  45.         else if (@operator is not BaseSearchOperator.In && keys is { Count: > 1 })
  46.         {
  47.             throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时必须设置一个元素。", nameof(keys));
  48.         }
  49.         else if (@operator is not (BaseSearchOperator.In or BaseSearchOperator.Equal) && keys.Any(static n => Equals(n, null)))
  50.         {
  51.             throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时元素的值不能为空。", nameof(keys));
  52.         }
  53.         Keys = keys;
  54.         Operator = @operator;
  55.         Reverse = reverse;
  56.     }
  57.     /// <inheritdoc/>
  58.     public virtual ImmutableList<T?> Keys { get; }
  59.     /// <inheritdoc/>
  60.     public virtual BaseSearchOperator Operator { get; }
  61.     /// <inheritdoc/>
  62.     public virtual bool Reverse { get; }
  63.     /// <inheritdoc/>
  64.     /// <exception cref="InvalidOperationException"></exception>
  65.     /// <exception cref="InvalidDataException"></exception>
  66.     public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccessor)
  67.     {
  68.         if (Keys.Any(static n => n is null)) throw new InvalidOperationException("不能使用值为空的元素搜索值不能为空的成员。");
  69.         Expression newBody = Operator switch
  70.         {
  71.             BaseSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(Keys.First(), _baseType)),
  72.             BaseSearchOperator.In => Expression.Call(null, _enumerableContains, [Expression.Constant(Keys.Cast<T>().ToList(), typeof(IEnumerable<T>)), memberAccessor.Body]),
  73.             _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  74.         };
  75.         if (Reverse) newBody = Expression.Not(newBody);
  76.         return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
  77.     }
  78.     /// <inheritdoc/>
  79.     /// <exception cref="InvalidOperationException"></exception>
  80.     /// <exception cref="InvalidDataException"></exception>
  81.     public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, T?>> memberAccessor)
  82.     {
  83.         Expression newBody = Operator switch
  84.         {
  85.             BaseSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(Keys.First(), _nullableType)),
  86.             BaseSearchOperator.In => Expression.Call(null, _enumerableNullableContains, [Expression.Constant(Keys, typeof(IEnumerable<T?>)), memberAccessor.Body]),
  87.             _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  88.         };
  89.         if (Reverse) newBody = Expression.Not(newBody);
  90.         return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
  91.     }
  92. }
  93. /// <summary>
  94. /// 布尔搜索过滤器
  95. /// </summary>
  96. public record BoolSearchFilter : StructSearchFilter<bool>
  97. {
  98.     /// <inheritdoc/>
  99.     public BoolSearchFilter(
  100.         ImmutableList<bool?> keys,
  101.         [EnumDataType(typeof(BaseSearchOperator))]
  102.         BaseSearchOperator @operator = BaseSearchOperator.Equal,
  103.         bool reversePredicate = false) : base(keys, @operator, reversePredicate)
  104.     {
  105.     }
  106. }
  107. /// <summary>
  108. /// 可排序数字搜索过滤器
  109. /// </summary>
  110. /// <typeparam name="TNumber">数字的类型</typeparam>
  111. public record NumberSearchFilter<TNumber>
  112.     : IStructComposableFilterPredicateBuilder<TNumber>
  113.     , ISearchFilterStructKey<TNumber>
  114.     , ISearchFilterOperator<ComparableNumberSearchOperator>
  115.     , IPredicateReversible
  116.     where TNumber : struct, IComparisonOperators<TNumber, TNumber, bool>
  117. {
  118.     /// <summary>
  119.     /// 初始化一个新实例
  120.     /// </summary>
  121.     /// <param name="keys">搜索关键字</param>
  122.     /// <param name="operator">搜索操作符</param>
  123.     /// <param name="reverse">是否反转条件</param>
  124.     /// <exception cref="ArgumentException"></exception>
  125.     /// <exception cref="InvalidEnumArgumentException"></exception>
  126.     public NumberSearchFilter(
  127.         ImmutableList<TNumber?> keys,
  128.         [EnumDataType(typeof(ComparableNumberSearchOperator))]
  129.         ComparableNumberSearchOperator @operator = ComparableNumberSearchOperator.Equal,
  130.         bool reverse = false)
  131.     {
  132.         ArgumentNullException.ThrowIfNull(nameof(keys));
  133.         if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
  134.         if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());
  135.         string? message = GetKeysCheckMessage(keys, @operator);
  136.         if (message is not null) throw new ArgumentException(message, nameof(keys));
  137.         Keys = keys;
  138.         Operator = @operator;
  139.         Reverse = reverse;
  140.     }
  141.     /// <inheritdoc/>
  142.     public virtual ImmutableList<TNumber?> Keys { get; }
  143.     /// <inheritdoc/>
  144.     public virtual ComparableNumberSearchOperator Operator { get; }
  145.     /// <inheritdoc/>
  146.     public virtual bool Reverse { get; }
  147.     /// <inheritdoc/>
  148.     /// <exception cref="InvalidOperationException"></exception>
  149.     /// <exception cref="InvalidDataException"></exception>
  150.     public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, TNumber>> memberAccessor)
  151.     {
  152.         NullKeyCheck(Keys);
  153.         var where = GetWherePredicateExtension(Keys, Operator, memberAccessor);
  154.         if (Reverse) where = where.Not();
  155.         return where;
  156.     }
  157.     /// <inheritdoc/>
  158.     /// <exception cref="InvalidOperationException"></exception>
  159.     /// <exception cref="InvalidDataException"></exception>
  160.     public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, TNumber?>> memberAccessor)
  161.     {
  162.         var where = GetWherePredicateExtension(Keys, Operator, memberAccessor);
  163.         if (Reverse) where = where.Not();
  164.         return where;
  165.     }
  166. }
  167. /// <summary>
  168. /// 字符串搜索过滤器
  169. /// </summary>
  170. public record StringSearchFilter
  171.     : IComposableFilterPredicateBuilder<string>
  172.     , ISearchFilterOperator<StringSearchOperator>
  173.     , ISearchFilterClassKey<string>
  174.     , IPredicateReversible
  175. {
  176.     private static readonly MethodInfo _contains = typeof(string)
  177.         .GetMethod(
  178.             nameof(string.Contains),
  179.             BindingFlags.Public | BindingFlags.Instance,
  180.             [typeof(string)]
  181.         )!;
  182.     private static readonly MethodInfo _startsWith = typeof(string)
  183.         .GetMethod(
  184.             nameof(string.StartsWith),
  185.             BindingFlags.Public | BindingFlags.Instance,
  186.             [typeof(string)]
  187.         )!;
  188.     private static readonly MethodInfo _endsWith = typeof(string)
  189.         .GetMethod(
  190.             nameof(string.EndsWith),
  191.             BindingFlags.Public | BindingFlags.Instance,
  192.             [typeof(string)]
  193.         )!;
  194.     private static readonly MethodInfo _equals = typeof(string)
  195.         .GetMethod(
  196.             nameof(string.Equals),
  197.             BindingFlags.Public | BindingFlags.Instance,
  198.             [typeof(string)]
  199.         )!;
  200.     private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
  201.         .GetMethods()
  202.         .Where(static m => m.Name is nameof(Enumerable.Contains))
  203.         .Single(static m => m.GetParameters().Length is 2)
  204.         .MakeGenericMethod([typeof(string)]);
  205.     /// <summary>
  206.     /// 初始化一个新实例
  207.     /// </summary>
  208.     /// <param name="keys">搜索关键字</param>
  209.     /// <param name="operator">搜索操作符</param>
  210.     /// <param name="reverse">是否反转条件</param>
  211.     /// <exception cref="ArgumentException"></exception>
  212.     /// <exception cref="InvalidEnumArgumentException"></exception>
  213.     public StringSearchFilter(
  214.         ImmutableList<string?> keys,
  215.         [EnumDataType(typeof(StringSearchOperator))]
  216.         StringSearchOperator @operator = StringSearchOperator.Contains,
  217.         bool reverse = false)
  218.     {
  219.         ArgumentNullException.ThrowIfNull(nameof(keys));
  220.         if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
  221.         if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());
  222.         string? exceptionHint = null;
  223.         switch (@operator)
  224.         {
  225.             case StringSearchOperator.Equal:
  226.                 if (keys is { Count: > 1 })
  227.                 {
  228.                     exceptionHint = $"必须设置一个元素。";
  229.                     goto default;
  230.                 }
  231.                 break;
  232.             case StringSearchOperator.In:
  233.                 if (keys is { Count: < 2 })
  234.                 {
  235.                     exceptionHint = $"必须设置多个元素。";
  236.                     goto default;
  237.                 }
  238.                 break;
  239.             case StringSearchOperator.Contains:
  240.                 goto case StringSearchOperator.Equal;
  241.             case StringSearchOperator.EqualContains:
  242.                 if (keys is { Count: < 2 })
  243.                 {
  244.                     exceptionHint = $"必须设置多个元素。";
  245.                     goto default;
  246.                 }
  247.                 else if (keys.Any(static key => key is null))
  248.                 {
  249.                     exceptionHint = $"元素不能为空。";
  250.                     goto default;
  251.                 }
  252.                 break;
  253.             case StringSearchOperator.InContains:
  254.                 goto case StringSearchOperator.EqualContains;
  255.             case StringSearchOperator.StartsWith:
  256.                 if (keys is { Count: > 2 })
  257.                 {
  258.                     exceptionHint = $"必须设置一个元素。";
  259.                     goto default;
  260.                 }
  261.                 else if (keys.Any(static key => key is null))
  262.                 {
  263.                     exceptionHint = $"元素不能为空。";
  264.                     goto default;
  265.                 }
  266.                 break;
  267.             case StringSearchOperator.InStartsWith:
  268.                 goto case StringSearchOperator.EqualContains;
  269.             case StringSearchOperator.EndsWith:
  270.                 goto case StringSearchOperator.StartsWith;
  271.             case StringSearchOperator.InEndsWith:
  272.                 goto case StringSearchOperator.EqualContains;
  273.             default:
  274.                 exceptionHint ??= "的元素数量错误。";
  275.                 throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时{exceptionHint}", nameof(keys));
  276.         };
  277.         Keys = keys;
  278.         Operator = @operator;
  279.         Reverse = reverse;
  280.     }
  281.     /// <inheritdoc/>
  282.     public virtual ImmutableList<string?> Keys { get; }
  283.     /// <inheritdoc/>
  284.     public virtual StringSearchOperator Operator { get; }
  285.     /// <inheritdoc/>
  286.     public virtual bool Reverse { get; }
  287.     /// <inheritdoc/>
  288.     /// <exception cref="InvalidDataException"></exception>
  289.     public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, string>> memberAccessor)
  290.     {
  291.         (MethodInfo method, object? value, Type type) = Operator switch
  292.         {
  293.             StringSearchOperator.Equal => (_equals, (object?)Keys![0], typeof(string)),
  294.             StringSearchOperator.In => (_enumerableContains, Keys, typeof(IEnumerable<string?>)),
  295.             StringSearchOperator.Contains => (_contains, Keys![0], typeof(string)),
  296.             StringSearchOperator.EqualContains => (_contains, Keys, typeof(string)),
  297.             StringSearchOperator.InContains => (_contains, Keys, typeof(string)),
  298.             StringSearchOperator.StartsWith => (_startsWith, Keys![0], typeof(string)),
  299.             StringSearchOperator.InStartsWith => (_startsWith, Keys, typeof(string)),
  300.             StringSearchOperator.EndsWith => (_endsWith, Keys![0], typeof(string)),
  301.             StringSearchOperator.InEndsWith => (_endsWith, Keys, typeof(string)),
  302.             _ => throw new InvalidDataException(nameof(StringSearchOperator))
  303.         };
  304.         Expression newBody;
  305.         switch (Operator)
  306.         {
  307.             case StringSearchOperator.Equal:
  308.                 newBody = Expression.Call(memberAccessor.Body, method, Expression.Constant(value, type));
  309.                 break;
  310.             case StringSearchOperator.In:
  311.                 newBody = Expression.Call(null, method, [Expression.Constant(value, type), memberAccessor.Body]);
  312.                 break;
  313.             case StringSearchOperator.Contains:
  314.                 goto case StringSearchOperator.Equal;
  315.             case StringSearchOperator.EqualContains:
  316.                 newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.And);
  317.                 break;
  318.             case StringSearchOperator.InContains:
  319.                 newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.Or);
  320.                 break;
  321.             case StringSearchOperator.StartsWith:
  322.                 goto case StringSearchOperator.Equal;
  323.             case StringSearchOperator.InStartsWith:
  324.                 newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.Or);
  325.                 break;
  326.             case StringSearchOperator.EndsWith:
  327.                 goto case StringSearchOperator.Equal;
  328.             case StringSearchOperator.InEndsWith:
  329.                 goto case StringSearchOperator.InStartsWith;
  330.             default:
  331.                 throw new InvalidDataException(nameof(StringSearchOperator));
  332.         }
  333.         if (Reverse) newBody = Expression.Not(newBody);
  334.         return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
  335.         static Expression CombineIn(IReadOnlyList<string?> keys, Expression instance, MethodInfo method, Type type, PredicateCombineKind combineKind)
  336.         {
  337.             Expression expr = Expression.Call(instance, method, Expression.Constant(keys[0], type));
  338.             foreach (var key in keys.Skip(1))
  339.             {
  340.                 expr = combineKind switch
  341.                 {
  342.                     PredicateCombineKind.And => Expression.AndAlso(expr, Expression.Call(instance, method, Expression.Constant(key, type))),
  343.                     PredicateCombineKind.Or => Expression.OrElse(expr, Expression.Call(instance, method, Expression.Constant(key, type))),
  344.                     _ => throw new NotImplementedException(),
  345.                 };
  346.             }
  347.             return expr;
  348.         }
  349.     }
  350. }
复制代码
对于集合类型的属性,笔者实现了计数和比例比较,用于筛选符合条件的元素数量或占比是否符合条件。如果必要,各种聚合条件也应该可以实现,此处不再枚举。TryUseAnyCall()方法对计数条件实验利用Any()更换Count(),比方Count > 0等价于Any(),这可以在EF Core中天生更高效的SQL(据说EF Core准备在内部添加这个优化)。
辅助类型
  1. /// <summary>
  2. /// 复杂类型的集合属性搜索过滤器
  3. /// </summary>
  4. /// <typeparam name="TQueryBuilder">要搜索的集合元素类型的查询类型</typeparam>
  5. /// <typeparam name="T">要搜索的元素类型</typeparam>
  6. public record CollectionMemberSearchFilter<TQueryBuilder, T>
  7.     : ICollectionComposableFilterPredicateBuilder<T>
  8.     , IPredicateReversible
  9.     where TQueryBuilder : IFilterPredicateBuilder<T>
  10. {
  11.     private static readonly MethodInfo _asQueryableOfT = typeof(Queryable)
  12.         .GetMethods()
  13.         .Single(static m => m.Name is nameof(Queryable.AsQueryable) && m.IsGenericMethod);
  14.     private static readonly MethodInfo _queryableWhereOfT = typeof(Queryable)
  15.         .GetMethods()
  16.         .Single(static m =>
  17.         {
  18.             return m.Name is nameof(Queryable.Where)
  19.                 && m.GetParameters()[1]
  20.                     .ParameterType
  21.                     .GenericTypeArguments[0]
  22.                     .GenericTypeArguments
  23.                     .Length is 2;
  24.         });
  25.     private static readonly MethodInfo _queryableCountOfT = typeof(Queryable)
  26.         .GetMethods()
  27.         .Single(static m => m.Name is nameof(Queryable.Count) && m.GetParameters().Length is 1);
  28.     private static readonly MethodInfo _queryableAnyOfT = typeof(Queryable)
  29.         .GetMethods()
  30.         .Single(static m => m.Name is nameof(Queryable.Any) && m.GetParameters().Length is 1);
  31.     private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
  32.         .GetMethods()
  33.         .Where(static m => m.Name is nameof(Enumerable.Contains))
  34.         .Single(static m => m.GetParameters().Length is 2)
  35.         .MakeGenericMethod([typeof(T)]);
  36.     /// <summary>
  37.     /// 元素的查询
  38.     /// </summary>
  39.     public TQueryBuilder? Query { get; }
  40.     /// <summary>
  41.     /// 计数搜索过滤器
  42.     /// </summary>
  43.     /// <remarks>和<see cref="Percent"/>只能存在一个。</remarks>
  44.     public NumberSearchFilter<int>? Count { get; }
  45.     /// <summary>
  46.     /// 比例搜索过滤器
  47.     /// </summary>
  48.     /// <remarks>
  49.     /// 和<see cref="Count"/>只能存在一个。
  50.     /// 如果存在,则<see cref="Query"/>也必须同时存在。
  51.     /// </remarks>
  52.     public NumberSearchFilter<double>? Percent { get; }
  53.     /// <inheritdoc/>
  54.     public bool Reverse { get; }
  55.     /// <summary>
  56.     /// 初始化新的实例
  57.     /// </summary>
  58.     /// <param name="query">元素的查询</param>
  59.     /// <param name="count">计数搜索过滤器</param>
  60.     /// <exception cref="ArgumentNullException"></exception>
  61.     public CollectionMemberSearchFilter(
  62.         TQueryBuilder? query,
  63.         NumberSearchFilter<int> count,
  64.         bool reverse = false) : this(query, count, null, reverse)
  65.     {
  66.     }
  67.     /// <summary>
  68.     /// 初始化新的实例
  69.     /// </summary>
  70.     /// <param name="query">元素的查询</param>
  71.     /// <param name="percent">比例搜索过滤器</param>
  72.     /// <exception cref="ArgumentNullException"></exception>
  73.     public CollectionMemberSearchFilter(
  74.         TQueryBuilder query,
  75.         NumberSearchFilter<double> percent,
  76.         bool reverse = false) : this(query, null, percent, reverse)
  77.     {
  78.     }
  79.     /// <summary>
  80.     /// 初始化新的实例
  81.     /// </summary>
  82.     /// <param name="query">元素的查询</param>
  83.     /// <param name="count">计数搜索过滤器</param>
  84.     /// <param name="percent">比例搜索过滤器</param>
  85.     /// <exception cref="ArgumentException"></exception>
  86.     [JsonConstructor]
  87.     public CollectionMemberSearchFilter(
  88.         TQueryBuilder? query,
  89.         NumberSearchFilter<int>? count = null,
  90.         NumberSearchFilter<double>? percent = null,
  91.         bool reverse = false)
  92.     {
  93.         if (count is null && percent is null || count is not null && percent is not null)
  94.         {
  95.             throw new ArgumentException($"{nameof(count)} 和 {nameof(percent)} 必须设置且只能设置其中一个。");
  96.         }
  97.         if (percent is not null && query is null)
  98.         {
  99.             throw new ArgumentException($"{nameof(percent)} 和 {nameof(query)} 必须同时设置。");
  100.         }
  101.         Query = query;
  102.         Count = count;
  103.         Percent = percent;
  104.         Reverse = reverse;
  105.     }
  106.     /// <inheritdoc/>
  107.     /// <exception cref="ArgumentException"></exception>
  108.     /// <exception cref="InvalidDataException"></exception>
  109.     public Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, IEnumerable<T>>> memberAccessor)
  110.     {
  111.         ArgumentNullException.ThrowIfNull(memberAccessor);
  112.         var asQueryable = _asQueryableOfT.MakeGenericMethod(typeof(T));
  113.         Expression queryable = Expression.Call(null, asQueryable, [memberAccessor.Body]);
  114.         Expression originalQueryable = queryable;
  115.         var queryCount = _queryableCountOfT.MakeGenericMethod(typeof(T));
  116.         Expression allCount = Expression.Call(null, queryCount, queryable);
  117.         Expression? whereCount = null;
  118.         var where = Query?.GetWherePredicate();
  119.         if (where != null)
  120.         {
  121.             var queryableWhere = _queryableWhereOfT.MakeGenericMethod(typeof(T));
  122.             queryable = Expression.Call(null, queryableWhere, [queryable, where]);
  123.             whereCount = Expression.Call(null, queryCount, queryable);
  124.         }
  125.         Expression? resultBody = null;
  126.         if (Count is not null)
  127.         {
  128.             var usedCount = whereCount ?? allCount;
  129.             resultBody = Count.Operator switch
  130.             {
  131.                 ComparableNumberSearchOperator.Equal => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
  132.                     ? anyCall
  133.                     : Expression.Equal(usedCount, Expression.Constant(Count.Keys[0])),
  134.                 ComparableNumberSearchOperator.In =>
  135.                     Expression.Call(
  136.                         null,
  137.                         _enumerableContains,
  138.                         [Expression.Constant(Count.Keys, typeof(IEnumerable<int>)), usedCount]
  139.                     ),
  140.                 ComparableNumberSearchOperator.LessThan => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
  141.                     ? anyCall
  142.                     : Expression.LessThan(usedCount, Expression.Constant(Count.Keys[0])),
  143.                 ComparableNumberSearchOperator.LessThanOrEqual => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
  144.                     ? anyCall
  145.                     : Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
  146.                 ComparableNumberSearchOperator.GreaterThan => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
  147.                     ? anyCall
  148.                     : Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
  149.                 ComparableNumberSearchOperator.GreaterThanOrEqual => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
  150.                     ? anyCall
  151.                     : Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
  152.                 ComparableNumberSearchOperator.BetweenOpen =>
  153.                     Expression.AndAlso(
  154.                         Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
  155.                         Expression.LessThan(usedCount, Expression.Constant(Count.Keys[1]))
  156.                     ),
  157.                 ComparableNumberSearchOperator.BetweenLeftClosed =>
  158.                     Expression.AndAlso(
  159.                         Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
  160.                         Expression.LessThan(usedCount, Expression.Constant(Count.Keys[1]))
  161.                     ),
  162.                 ComparableNumberSearchOperator.BetweenRightClosed =>
  163.                     Expression.AndAlso(
  164.                         Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
  165.                         Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[1]))
  166.                     ),
  167.                 ComparableNumberSearchOperator.BetweenClosed =>
  168.                     Expression.AndAlso(
  169.                         Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
  170.                         Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[1]))
  171.                     ),
  172.                 _ => throw new InvalidDataException(nameof(Count.Operator)),
  173.             };
  174.             if (Count.Reverse) resultBody = Expression.Not(resultBody);
  175.         }
  176.         else if (Percent is not null)
  177.         {
  178.             Debug.Assert(whereCount is not null);
  179.             Expression doubleAllCount = Expression.Convert(allCount, typeof(double));
  180.             whereCount = Expression.Convert(whereCount, typeof(double));
  181.             Expression usedPercent = Expression.Divide(whereCount, doubleAllCount);
  182.             var queryableAny = _queryableAnyOfT.MakeGenericMethod(typeof(T));
  183.             usedPercent = Expression.Condition(Expression.Not(Expression.Call(null, queryableAny, originalQueryable)), Expression.Constant(0.0), usedPercent);
  184.             resultBody = Percent.Operator switch
  185.             {
  186.                 ComparableNumberSearchOperator.Equal => Expression.Equal(usedPercent, Expression.Constant(Percent.Keys[0])),
  187.                 ComparableNumberSearchOperator.In =>
  188.                     Expression.Call(
  189.                         null,
  190.                         _enumerableContains,
  191.                         [Expression.Constant(Percent.Keys, typeof(IEnumerable<double>)), usedPercent]
  192.                     ),
  193.                 ComparableNumberSearchOperator.LessThan => Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[0])),
  194.                 ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
  195.                 ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
  196.                 ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
  197.                 ComparableNumberSearchOperator.BetweenOpen =>
  198.                     Expression.AndAlso(
  199.                         Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
  200.                         Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[1]))
  201.                     ),
  202.                 ComparableNumberSearchOperator.BetweenLeftClosed =>
  203.                     Expression.AndAlso(
  204.                         Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
  205.                         Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[1]))
  206.                     ),
  207.                 ComparableNumberSearchOperator.BetweenRightClosed =>
  208.                     Expression.AndAlso(
  209.                         Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
  210.                         Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[1]))
  211.                     ),
  212.                 ComparableNumberSearchOperator.BetweenClosed =>
  213.                     Expression.AndAlso(
  214.                         Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
  215.                         Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[1]))
  216.                     ),
  217.                 _ => throw new InvalidDataException(nameof(Percent.Operator)),
  218.             };
  219.             if (Percent.Reverse) resultBody = Expression.Not(resultBody);
  220.         }
  221.         Debug.Assert(resultBody is not null);
  222.         var result = Expression.Lambda<Func<TOwner, bool>>(resultBody, memberAccessor.Parameters);
  223.         return this.ApplyReversiblePredicate(result);
  224.         static bool TryUseAnyCall(
  225.             ComparableNumberSearchOperator @operator,
  226.             int? key,
  227.             Expression toCallAny,
  228.             [NotNullWhen(true)] out Expression? result)
  229.         {
  230.             ArgumentNullException.ThrowIfNull(toCallAny);
  231.             (bool shouldUseAny, bool shouldReverseAny) = (@operator, key) switch
  232.             {
  233.                 (ComparableNumberSearchOperator.Equal, 0) => (true, true),
  234.                 (ComparableNumberSearchOperator.LessThan, 1) => (true, true),
  235.                 (ComparableNumberSearchOperator.LessThanOrEqual, 0) => (true, true),
  236.                 (ComparableNumberSearchOperator.GreaterThan, 0) => (true, false),
  237.                 (ComparableNumberSearchOperator.GreaterThanOrEqual, 1) => (true, false),
  238.                 _ => (false, false),
  239.             };
  240.             result = null;
  241.             if (shouldUseAny)
  242.             {
  243.                 result = Expression.Call(
  244.                     null,
  245.                     _queryableAnyOfT.MakeGenericMethod(typeof(T)),
  246.                     [toCallAny]
  247.                 );
  248.                 if (shouldReverseAny)
  249.                 {
  250.                     result = Expression.Not(result);
  251.                 }
  252.                 return true;
  253.             }
  254.             return false;
  255.         }
  256.     }
  257. }
复制代码
辅助类型用于统一定义表达式的拼接方式等,因为这些拼接对于大多数基础数据类型来说都是通用的。
  1. internal static class ScalarSearchFilterExtensions
  2. {
  3.     private static readonly MethodInfo _enumerableContainsOfT = typeof(Enumerable)
  4.         .GetMethods()
  5.         .Where(static m => m.Name is nameof(Enumerable.Contains))
  6.         .Single(static m => m.GetParameters().Length is 2);
  7.     internal static string? GetKeysCheckMessage<T>(ICollection<T> keys, ComparableNumberSearchOperator @operator)
  8.     {
  9.         string? exceptionHint = null;
  10.         switch (@operator)
  11.         {
  12.             case ComparableNumberSearchOperator.Equal:
  13.                 if (keys is { Count: > 1 })
  14.                 {
  15.                     exceptionHint = $"必须设置一个元素。";
  16.                 }
  17.                 break;
  18.             case ComparableNumberSearchOperator.In:
  19.                 if (keys is { Count: < 2 })
  20.                 {
  21.                     exceptionHint = $"必须设置多个元素。";
  22.                 }
  23.                 break;
  24.             case ComparableNumberSearchOperator.LessThan:
  25.                 goto case ComparableNumberSearchOperator.Equal;
  26.             case ComparableNumberSearchOperator.LessThanOrEqual:
  27.                 goto case ComparableNumberSearchOperator.Equal;
  28.             case ComparableNumberSearchOperator.GreaterThan:
  29.                 goto case ComparableNumberSearchOperator.Equal;
  30.             case ComparableNumberSearchOperator.GreaterThanOrEqual:
  31.                 goto case ComparableNumberSearchOperator.Equal;
  32.             case ComparableNumberSearchOperator.BetweenOpen:
  33.                 goto case ComparableNumberSearchOperator.BetweenClosed;
  34.             case ComparableNumberSearchOperator.BetweenLeftClosed:
  35.                 goto case ComparableNumberSearchOperator.BetweenClosed;
  36.             case ComparableNumberSearchOperator.BetweenRightClosed:
  37.                 goto case ComparableNumberSearchOperator.BetweenClosed;
  38.             case ComparableNumberSearchOperator.BetweenClosed:
  39.                 if (keys is { Count: not 2 })
  40.                 {
  41.                     exceptionHint = $"必须设置两个元素。";
  42.                 }
  43.                 break;
  44.             case ComparableNumberSearchOperator.InBetweenOpen:
  45.                 if (keys is { Count: < 4 } || keys.Count % 2 != 0)
  46.                 {
  47.                     exceptionHint = $"必须设置不少于四个的偶数个元素。";
  48.                 }
  49.                 break;
  50.             case ComparableNumberSearchOperator.InBetweenLeftClosed:
  51.                 goto case ComparableNumberSearchOperator.InBetweenOpen;
  52.             case ComparableNumberSearchOperator.InBetweenRightClosed:
  53.                 goto case ComparableNumberSearchOperator.InBetweenOpen;
  54.             case ComparableNumberSearchOperator.InBetweenClosed:
  55.                 goto case ComparableNumberSearchOperator.InBetweenOpen;
  56.             default:
  57.                 exceptionHint = "的元素数量错误。";
  58.                 break;
  59.         };
  60.         if (exceptionHint is not null) return $"当 {nameof(@operator)} 的值为 {@operator} 时{exceptionHint}";
  61.         else return null;
  62.     }
  63.     internal static void NullKeyCheck<T>(IReadOnlyList<T?> keys)
  64.         where T : struct
  65.     {
  66.         if (keys.Any(static n => n is null)) throw new InvalidOperationException("不能使用值为空的元素搜索值不能为空的成员。");
  67.     }
  68.     internal static Expression<Func<TOwner, bool>> GetWherePredicateExtension<TOwner, TNumber>(
  69.         IReadOnlyList<TNumber?> keys,
  70.         ComparableNumberSearchOperator @operator,
  71.         Expression<Func<TOwner, TNumber>> memberAccessor)
  72.         where TNumber : struct
  73.     {
  74.         var typeOfNumber = typeof(TNumber);
  75.         var _enumerableContains = @operator is ComparableNumberSearchOperator.In ? _enumerableContainsOfT.MakeGenericMethod([typeOfNumber]) : null;
  76.         Expression newBody = @operator switch
  77.         {
  78.             ComparableNumberSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
  79.             ComparableNumberSearchOperator.In => Expression.Call(null, _enumerableContains!, [Expression.Constant(keys.Cast<TNumber>().ToList(), typeof(IEnumerable<TNumber>)), memberAccessor.Body]),
  80.             ComparableNumberSearchOperator.LessThan => Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
  81.             ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
  82.             ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
  83.             ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
  84.             ComparableNumberSearchOperator.BetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
  85.             ComparableNumberSearchOperator.BetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
  86.             ComparableNumberSearchOperator.BetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
  87.             ComparableNumberSearchOperator.BetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
  88.             ComparableNumberSearchOperator.InBetweenOpen => CombineInBetweenBody(keys, @operator, memberAccessor),
  89.             ComparableNumberSearchOperator.InBetweenLeftClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
  90.             ComparableNumberSearchOperator.InBetweenRightClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
  91.             ComparableNumberSearchOperator.InBetweenClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
  92.             _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  93.         };
  94.         return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
  95.     }
  96.     internal static Expression<Func<TOwner, bool>> GetWherePredicateExtension<TOwner, TNumber>(
  97.         IReadOnlyList<TNumber?> keys,
  98.         ComparableNumberSearchOperator @operator,
  99.         Expression<Func<TOwner, TNumber?>> memberAccessor)
  100.         where TNumber : struct
  101.     {
  102.         var typeOfNullableNumber = typeof(TNumber?);
  103.         var enumerableContains = @operator is ComparableNumberSearchOperator.In ? _enumerableContainsOfT.MakeGenericMethod([typeOfNullableNumber]) : null;
  104.         Expression newBody = @operator switch
  105.         {
  106.             ComparableNumberSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
  107.             ComparableNumberSearchOperator.In => Expression.Call(null, enumerableContains!, [Expression.Constant(keys, typeof(IEnumerable<TNumber?>)), memberAccessor.Body]),
  108.             ComparableNumberSearchOperator.LessThan => Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
  109.             ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
  110.             ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
  111.             ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
  112.             ComparableNumberSearchOperator.BetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
  113.             ComparableNumberSearchOperator.BetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
  114.             ComparableNumberSearchOperator.BetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
  115.             ComparableNumberSearchOperator.BetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
  116.             ComparableNumberSearchOperator.InBetweenOpen => CombineInBetweenBody(keys, @operator, memberAccessor),
  117.             ComparableNumberSearchOperator.InBetweenLeftClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
  118.             ComparableNumberSearchOperator.InBetweenRightClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
  119.             ComparableNumberSearchOperator.InBetweenClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
  120.             _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  121.         };
  122.         return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
  123.     }
  124.     private static Expression CombineInBetweenBody<TOwner, TNumber>(
  125.         IReadOnlyList<TNumber?> keys,
  126.         ComparableNumberSearchOperator @operator,
  127.         Expression<Func<TOwner, TNumber>> memberAccessor)
  128.         where TNumber : struct
  129.     {
  130.         var typeOfNumber = typeof(TNumber);
  131.         var keysGroups = keys.Chunk(2);
  132.         Expression newBody = @operator switch
  133.         {
  134.             ComparableNumberSearchOperator.InBetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
  135.             ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
  136.             ComparableNumberSearchOperator.InBetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
  137.             ComparableNumberSearchOperator.InBetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
  138.             _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  139.         };
  140.         foreach (var inKeys in keysGroups.Skip(1))
  141.         {
  142.             newBody = @operator switch
  143.             {
  144.                 ComparableNumberSearchOperator.InBetweenOpen => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
  145.                 ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
  146.                 ComparableNumberSearchOperator.InBetweenRightClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
  147.                 ComparableNumberSearchOperator.InBetweenClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
  148.                 _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  149.             };
  150.         }
  151.         return newBody;
  152.     }
  153.     private static Expression CombineInBetweenBody<TOwner, TNumber>(
  154.         IReadOnlyList<TNumber?> keys,
  155.         ComparableNumberSearchOperator @operator,
  156.         Expression<Func<TOwner, TNumber?>> memberAccessor)
  157.         where TNumber : struct
  158.     {
  159.         var typeOfNullableNumber = typeof(TNumber?);
  160.         var keysGroups = keys.Chunk(2);
  161.         Expression newBody = @operator switch
  162.         {
  163.             ComparableNumberSearchOperator.InBetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
  164.             ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
  165.             ComparableNumberSearchOperator.InBetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
  166.             ComparableNumberSearchOperator.InBetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
  167.             _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  168.         };
  169.         foreach (var inKeys in keysGroups.Skip(1))
  170.         {
  171.             newBody = @operator switch
  172.             {
  173.                 ComparableNumberSearchOperator.InBetweenOpen => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
  174.                 ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
  175.                 ComparableNumberSearchOperator.InBetweenRightClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
  176.                 ComparableNumberSearchOperator.InBetweenClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
  177.                 _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
  178.             };
  179.         }
  180.         return newBody;
  181.     }
  182. }
复制代码
专用于返回bool类型的表达式的合并扩展。
扩展复杂筛选支持

偶然查询条件可能比较复杂,单个天生器无法表达。因此必要一个用于形貌更复杂的条件的天生器,单个天生器无法形貌的环境利用多个天生器组合来实现。既然复杂条件是由单个条件组合而来,最好能复用已经定义好的单个筛选器。
  1. /// <summary>
  2. /// 合并表达式 And Or Not扩展方法
  3. /// </summary>
  4. public static class ExpressionExtensions
  5. {
  6.     /// <summary>
  7.     /// 合并表达式 expr1 AND expr2
  8.     /// </summary>
  9.     /// <typeparam name="T">表达式参数类型</typeparam>
  10.     /// <param name="expr1">待合并的表达式1</param>
  11.     /// <param name="expr2">待合并的表达式2</param>
  12.     /// <returns>合并的表达式</returns>
  13.     public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
  14.     {
  15.         ArgumentNullException.ThrowIfNull(nameof(expr1));
  16.         ArgumentNullException.ThrowIfNull(nameof(expr2));
  17.         ParameterReplaceExpressionVisitor visitor = new();
  18.         Dictionary<ParameterExpression, ParameterExpression> replaceMapping = [];
  19.         ParameterExpression newParameter = Expression.Parameter(typeof(T), expr1.Parameters[0].Name);
  20.         replaceMapping[expr1.Parameters[0]] = newParameter;
  21.         var left = visitor.ReplaceParameter(expr1, replaceMapping);
  22.         replaceMapping.Clear();
  23.         replaceMapping[expr2.Parameters[0]] = newParameter;
  24.         var right = visitor.ReplaceParameter(expr2, replaceMapping);
  25.         BinaryExpression body = Expression.AndAlso(left!.Body, right!.Body);
  26.         return Expression.Lambda<Func<T, bool>>(body, newParameter);
  27.     }
  28.     /// <summary>
  29.     /// 合并多个表达式 expr1 AND expr2 AND ...
  30.     /// </summary>
  31.     /// <typeparam name="T">表达式参数类型</typeparam>
  32.     /// <param name="source">待合并的表达式集合</param>
  33.     /// <returns>合并的表达式</returns>
  34.     public static Expression<Func<T, bool>>? AndAlsoAll<T>(this IEnumerable<Expression<Func<T, bool>>> source)
  35.     {
  36.         ArgumentNullException.ThrowIfNull(nameof(source));
  37.         if (!source.Any() || !source.Skip(1).Any()) return source.FirstOrDefault();
  38.         ParameterReplaceExpressionVisitor visitor = new();
  39.         Dictionary<ParameterExpression, ParameterExpression> replaceMapping = [];
  40.         var first = source.First();
  41.         ParameterExpression newParameter = Expression.Parameter(typeof(T), first.Parameters[0].Name);
  42.         replaceMapping[first.Parameters[0]] = newParameter;
  43.         var left = visitor.ReplaceParameter(first, replaceMapping);
  44.         List<LambdaExpression> rights = [];
  45.         foreach (var right in source.Skip(1))
  46.         {
  47.             replaceMapping.Clear();
  48.             replaceMapping[right.Parameters[0]] = newParameter;
  49.             rights.Add(visitor.ReplaceParameter(right, replaceMapping)!);
  50.         }
  51.         BinaryExpression body = Expression.AndAlso(left!.Body, rights[0].Body);
  52.         foreach (var right in rights.Skip(1))
  53.         {
  54.             body = Expression.AndAlso(body, right.Body);
  55.         }
  56.         return Expression.Lambda<Func<T, bool>>(body, newParameter);
  57.     }
  58.     /// <summary>
  59.     /// 合并表达式 expr1 OR expr2
  60.     /// </summary>
  61.     /// <typeparam name="T">表达式参数类型</typeparam>
  62.     /// <param name="expr1">待合并的表达式1</param>
  63.     /// <param name="expr2">待合并的表达式2</param>
  64.     /// <returns>合并的表达式</returns>
  65.     public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
  66.     {
  67.         ArgumentNullException.ThrowIfNull(nameof(expr1));
  68.         ArgumentNullException.ThrowIfNull(nameof(expr2));
  69.         ParameterReplaceExpressionVisitor visitor = new();
  70.         Dictionary<ParameterExpression, ParameterExpression> replaceMapping = [];
  71.         ParameterExpression newParameter = Expression.Parameter(typeof(T), expr1.Parameters[0].Name);
  72.         replaceMapping[expr1.Parameters[0]] = newParameter;
  73.         var left = visitor.ReplaceParameter(expr1, replaceMapping);
  74.         replaceMapping.Clear();
  75.         replaceMapping[expr2.Parameters[0]] = newParameter;
  76.         var right = visitor.ReplaceParameter(expr2, replaceMapping);
  77.         BinaryExpression body = Expression.OrElse(left!.Body, right!.Body);
  78.         return Expression.Lambda<Func<T, bool>>(body, newParameter);
  79.     }
  80.     /// <summary>
  81.     /// 合并多个表达式 expr1 OR expr2 OR ...
  82.     /// </summary>
  83.     /// <typeparam name="T">表达式参数类型</typeparam>
  84.     /// <returns>合并的表达式</returns>
  85.     public static Expression<Func<T, bool>>? OrElseAll<T>(this IEnumerable<Expression<Func<T, bool>>> source)
  86.     {
  87.         ArgumentNullException.ThrowIfNull(nameof(source));
  88.         if (!source.Any() || !source.Skip(1).Any()) return source.FirstOrDefault();
  89.         ParameterReplaceExpressionVisitor visitor = new();
  90.         Dictionary<ParameterExpression, ParameterExpression> replaceMapping = [];
  91.         var first = source.First();
  92.         ParameterExpression newParameter = Expression.Parameter(typeof(T), first.Parameters[0].Name);
  93.         replaceMapping[first.Parameters[0]] = newParameter;
  94.         var left = visitor.ReplaceParameter(first, replaceMapping);
  95.         List<LambdaExpression> rights = [];
  96.         foreach (var right in source.Skip(1))
  97.         {
  98.             replaceMapping.Clear();
  99.             replaceMapping[right.Parameters[0]] = newParameter;
  100.             rights.Add(visitor.ReplaceParameter(right, replaceMapping)!);
  101.         }
  102.         BinaryExpression body = Expression.OrElse(left!.Body, rights[0].Body);
  103.         foreach (var right in rights.Skip(1))
  104.         {
  105.             body = Expression.OrElse(body, right.Body);
  106.         }
  107.         return Expression.Lambda<Func<T, bool>>(body, newParameter);
  108.     }
  109.     /// <summary>
  110.     /// 表达式取非 NOT
  111.     /// </summary>
  112.     /// <typeparam name="T">表达式参数类型</typeparam>
  113.     /// <param name="expr">原表达式</param>
  114.     /// <returns>已经取非的表达式</returns>
  115.     public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expr)
  116.     {
  117.         ArgumentNullException.ThrowIfNull(nameof(expr));
  118.         UnaryExpression body = Expression.Not(expr.Body);
  119.         return Expression.Lambda<Func<T, bool>>(body, expr.Parameters[0]);
  120.     }
  121. }
  122. /// <summary>
  123. /// 替换表达式参数的访问器
  124. /// </summary>
  125. public sealed class ParameterReplaceExpressionVisitor : ExpressionVisitor
  126. {
  127.     private readonly Lock _lock = new();
  128.     private volatile bool _calledFromReplaceParameter = false;
  129.     private IReadOnlyDictionary<ParameterExpression, ParameterExpression>? _parameterReplaceMapping;
  130.     /// <summary>
  131.     /// 把指定表达式的参数替换为新的参数。
  132.     /// </summary>
  133.     /// <param name="expr">要替换参数的表达式。</param>
  134.     /// <param name="parameterReplaceMapping">替换用的新参数表达式映射。Key是表达式中的现有参数,Value是新参数。</param>
  135.     /// <returns>已替换参数的表达式。</returns>
  136.     /// <exception cref="ArgumentException"></exception>
  137.     /// <exception cref="ArgumentNullException"></exception>
  138.     public LambdaExpression? ReplaceParameter(LambdaExpression expr, IReadOnlyDictionary<ParameterExpression, ParameterExpression> parameterReplaceMapping)
  139.     {
  140.         ArgumentNullException.ThrowIfNull(expr);
  141.         ArgumentNullException.ThrowIfNull(parameterReplaceMapping);
  142.         if (expr.Parameters.Count == 0) throw new ArgumentException($"{nameof(expr)}不能是无参数表达式。", nameof(expr));
  143.         lock (_lock)
  144.         {
  145.             try
  146.             {
  147.                 // 复制参数避免多线程修改损坏参数
  148.                 var replaceCopy = parameterReplaceMapping.AsEnumerable().ToDictionary();
  149.                 if (replaceCopy.Count == 0) throw new ArgumentException($"{nameof(parameterReplaceMapping)}不能没有元素。", nameof(parameterReplaceMapping));
  150.                 if (expr.Parameters.Count < replaceCopy.Count) throw new ArgumentException($"{nameof(parameterReplaceMapping)}的元素数量不能大于表达式的参数数量。", nameof(parameterReplaceMapping));
  151.                 foreach (var mapping in replaceCopy)
  152.                 {
  153.                     if (!expr.Parameters.Contains(mapping.Key))
  154.                         throw new ArgumentException("被替换的参数必须是表达式使用的参数。", nameof(parameterReplaceMapping));
  155.                 }
  156.                 _parameterReplaceMapping = replaceCopy;
  157.                 _calledFromReplaceParameter = true;
  158.                 var result = Visit(expr);
  159.                 return result as LambdaExpression;
  160.             }
  161.             catch
  162.             {
  163.                 throw;
  164.             }
  165.             finally
  166.             {
  167.                 _calledFromReplaceParameter = false;
  168.                 _parameterReplaceMapping = null;
  169.             }
  170.         }
  171.     }
  172.     [return: NotNullIfNotNull(nameof(node))]
  173.     public override Expression? Visit(Expression? node)
  174.     {
  175.         if (!_calledFromReplaceParameter) throw new InvalidOperationException($"Don't call directly, call {nameof(ReplaceParameter)} instead.");
  176.         return base.Visit(node);
  177.     }
  178.     protected override Expression VisitParameter(ParameterExpression node)
  179.     {
  180.         if (_parameterReplaceMapping!.TryGetValue(node, out var newParameter)) return newParameter;
  181.         return base.VisitParameter(node);
  182.     }
  183. }
复制代码
高级查询接口允许组合和嵌套任意多个筛选器以实现更复杂的条件。
  1. /// <summary>
  2. /// 支持复杂条件嵌套的高级查询条件构造器接口
  3. /// </summary>
  4. /// <typeparam name="TFilterPredicateBuilder">基础查询条件构造器</typeparam>
  5. /// <typeparam name="T">要查询的数据类型</typeparam>
  6. public interface IAdvancedFilterPredicateBuilder<out TFilterPredicateBuilder, T> : IFilterPredicateBuilder<T>
  7.     where TFilterPredicateBuilder : IFilterPredicateBuilder<T>
  8. {
  9.     /// <summary>
  10.     /// 基础查询条件集合
  11.     /// </summary>
  12.     IReadOnlyList<TFilterPredicateBuilder>? Filters { get; }
  13.     /// <summary>
  14.     /// 高级查询条件组集合
  15.     /// </summary>
  16.     IReadOnlyList<IAdvancedFilterPredicateBuilder<TFilterPredicateBuilder, T>>? FilterGroups { get; }
  17. }
复制代码
单个条件可以获取对应的筛选表达式,那么这些条件的组合也就是对这些表达式进行组合。
添加排序和分页支持

分页的前提是排序,否则分页的效果不稳定。对于分页,通常还想获得符合条件的数据总量用于计算页数,但是排序并不影响总数计算。如果能在计算总数时忽略排序,仅在获取某一页数据时利用排序最好。这就要求我们分别存储和处理筛选条件和排序条件。筛选条件的问题已经在前面解决了,这里只必要关注排序条件的问题。
排序的概念接口
  1. /// <summary>
  2. /// 支持复杂条件嵌套的高级查询构造器
  3. /// </summary>
  4. /// <typeparam name="TQueryBuilder">基础查询构造器</typeparam>
  5. /// <typeparam name="T">要查询的数据类型</typeparam>
  6. /// <param name="Queries">基础查询条件集合</param>
  7. /// <param name="QueryGroups">高级查询条件组集合</param>
  8. /// <param name="CombineType">条件组合方式</param>
  9. /// <param name="Reverse">是否反转条件</param>
  10. public record AdvancedQueryBuilder<TQueryBuilder, T>(
  11.     ImmutableList<TQueryBuilder>? Queries = null,
  12.     ImmutableList<AdvancedQueryBuilder<TQueryBuilder, T>>? QueryGroups = null,
  13.     [EnumDataType(typeof(PredicateCombineKind))]
  14.     PredicateCombineKind? CombineType = PredicateCombineKind.And,
  15.     bool Reverse = false)
  16.     : IAdvancedFilterPredicateBuilder<TQueryBuilder, T>
  17.     , IPredicateReversible
  18.     where TQueryBuilder : IFilterPredicateBuilder<T>
  19. {
  20.     /// <inheritdoc/>
  21.     public IReadOnlyList<TQueryBuilder>? Filters => Queries;
  22.     /// <inheritdoc/>
  23.     public IReadOnlyList<IAdvancedFilterPredicateBuilder<TQueryBuilder, T>>? FilterGroups => QueryGroups;
  24.     /// <summary>
  25.     /// 获取查询条件
  26.     /// </summary>
  27.     /// <returns>组合完成的查询条件</returns>
  28.     public Expression<Func<T, bool>>? GetWherePredicate()
  29.     {
  30.         var where = CombinePredicates(Queries?.Select(static q => q.GetWherePredicate())
  31.             .Concat(QueryGroups?.Select(static qg => qg.GetWherePredicate()) ?? [])
  32.             .Where(static p => p is not null)!);
  33.         return this.ApplyReversiblePredicate(where);
  34.     }
  35.     /// <summary>
  36.     /// 组合查询条件
  37.     /// </summary>
  38.     /// <param name="predicates">待组合的子条件</param>
  39.     /// <returns></returns>
  40.     /// <exception cref="NotSupportedException"></exception>
  41.     protected Expression<Func<T, bool>>? CombinePredicates(IEnumerable<Expression<Func<T, bool>>>? predicates)
  42.     {
  43.         var predicate = predicates?.FirstOrDefault();
  44.         if (predicates?.Any() is true)
  45.         {
  46.             predicate = CombineType switch
  47.             {
  48.                 PredicateCombineKind.And => predicates?.AndAlsoAll(),
  49.                 PredicateCombineKind.Or => predicates?.OrElseAll(),
  50.                 _ => throw new NotSupportedException(CombineType.ToString()),
  51.             };
  52.         }
  53.         return predicate;
  54.     }
  55. }
复制代码
同样的,第一个接口只形貌如作甚查询附加排序,第二个接口形貌序列化传输的方式。LINQ中的排序方法参数是一个排序关键字属性访问表达式,属性的类型就是表达式的返回值类型,属性类型千变万化,因此只能利用返回object的表达式来存储。表达式关键字自己无法序列化传输,因此笔者选择利用枚举来指代表达式,这也同时限定了可用于排序的属性,有利于安全。
分页的概念接口

现在有两种流行的分页方式,偏移量分页和游标分页。偏移量分页支持随机页码跳转,也必要提前计算数据总数,数据量大或访问尾部页码时性能会下降。游标分页则是根据唯一键游标排序来进行,可以是两个游标之间的数据或单个游标和获取数据量的组合。在返回的效果中必要附带相邻页面的起始游标,因此实际查询数据时必要多查一条数据,多查的这一条数据只必要游标值,不必要数据自己。本文以偏移量分页为例。
  1. /// <summary>
  2. /// 排序查询构造器接口
  3. /// </summary>
  4. /// <typeparam name="T">查询的元素类型</typeparam>
  5. public interface IOrderedQueryBuilder<T>
  6. {
  7.     /// <summary>
  8.     /// 对查询应用排序
  9.     /// </summary>
  10.     /// <param name="query">原始查询</param>
  11.     /// <returns>已排序的查询</returns>
  12.     IOrderedQueryable<T> ApplyOrder(IQueryable<T> query);
  13. }
  14. /// <summary>
  15. /// 可排序查询接口
  16. /// </summary>
  17. /// <typeparam name="T">查询的元素类型</typeparam>
  18. /// <typeparam name="TOrderKey">可用的排序关键字枚举类型</typeparam>
  19. public interface IKeySelectorOrderedQueryBuilder<T, TOrderKey> : IOrderedQueryBuilder<T>
  20.     where TOrderKey : struct, Enum
  21. {
  22.     /// <summary>
  23.     /// 获取支持的排序关键字选择器
  24.     /// </summary>
  25.     IReadOnlyDictionary<TOrderKey, Expression<Func<T, object?>>> GetSupportedOrderKeySelectors();
  26.     /// <summary>
  27.     /// 排序关键字信息
  28.     /// </summary>
  29.     ImmutableList<OrderInfo<TOrderKey>>? OrderKeys { get; }
  30. }
  31. /// <summary>
  32. /// 排序信息
  33. /// </summary>
  34. /// <typeparam name="T">排序对象的类型</typeparam>
  35. /// <param name="Key">排序关键字</param>
  36. /// <param name="OrderKind">排序方式</param>
  37. public record OrderInfo<T>(
  38.     T Key,
  39.     [EnumDataType(typeof(OrderKind))]
  40.     OrderKind OrderKind = OrderKind.Asc)
  41.     where T : struct, Enum
  42. {
  43.     /// <summary>
  44.     /// 排序关键字
  45.     /// </summary>
  46.     public T Key { get; } = CheckOrderKey(Key);
  47.     private static T CheckOrderKey(T value)
  48.     {
  49.         if (!Enum.IsDefined(value)) throw new InvalidEnumArgumentException(nameof(Key), int.Parse(value.ToString()), typeof(T));
  50.         return value;
  51.     }
  52. }
  53. /// <summary>
  54. /// 排序方式
  55. /// </summary>
  56. public enum OrderKind
  57. {
  58.     /// <summary>
  59.     /// 升序
  60.     /// </summary>
  61.     Asc = 1,
  62.     /// <summary>
  63.     /// 降序
  64.     /// </summary>
  65.     Desc = 2,
  66. }
复制代码
完整的分页查询天生器
  1. /// <summary>
  2. /// 可页码分页查询接口
  3. /// </summary>
  4. public interface IOffsetPagingSupport
  5. {
  6.     /// <summary>
  7.     /// 分页信息
  8.     /// </summary>
  9.     OffsetPageInfo OffsetPage { get; }
  10. }
  11. /// <summary>
  12. /// 页码分页信息
  13. /// </summary>
  14. /// <param name="PageIndex">页码</param>
  15. /// <param name="PageSize">页面大小</param>
  16. public record OffsetPageInfo(
  17.     [Range(1, int.MaxValue, ErrorMessage = DataAnnotationErrorMessageDefaults.Range)] int PageIndex = 1,
  18.     [Range(1, int.MaxValue, ErrorMessage = DataAnnotationErrorMessageDefaults.Range)] int PageSize = 10
  19. )
  20. {
  21.     /// <summary>
  22.     /// 跳过的页数
  23.     /// </summary>
  24.     public int SkipedPageCount => PageIndex - 1;
  25.     /// <summary>
  26.     /// 跳过的元素数量
  27.     /// </summary>
  28.     public int SkipedElementCount => SkipedPageCount * PageSize;
  29. }
  30. /// <summary>
  31. /// 页码分页查询构造扩展
  32. /// </summary>
  33. public static class OffsetPageQueryBuilderExtensions
  34. {
  35.     /// <summary>
  36.     /// 页码分页
  37.     /// </summary>
  38.     /// <typeparam name="T">查询的元素类型</typeparam>
  39.     /// <param name="offsetPaging">分页信息</param>
  40.     /// <param name="query">要应用分页的查询</param>
  41.     /// <returns>已页码分页的查询</returns>
  42.     public static IQueryable<T> OffsetPage<T>(this IOffsetPagingSupport offsetPaging, IQueryable<T> query)
  43.     {
  44.         ArgumentNullException.ThrowIfNull(offsetPaging);
  45.         ArgumentNullException.ThrowIfNull(query);
  46.         var paging = offsetPaging.OffsetPage;
  47.         return query.Skip(paging.SkipedElementCount).Take(paging.PageSize);
  48.     }
  49. }
复制代码
此处的抽象基类不实现关键字排序是因为无法确定最终查询是否支持关键字排序,有可能是在代码中定义的静态排序规则。如果确实支持关键字排序,在最终类型上实现关键字排序接口即可。扩展类型则用于快速实现关键字排序表达式天生。
利用示例

演示用数据类型
  1. /// <summary>
  2. /// 分页查询构造器
  3. /// </summary>
  4. /// <typeparam name="TQueryBuilder">查询构造器类型</typeparam>
  5. /// <typeparam name="T">查询的数据类型</typeparam>
  6. /// <param name="Query">基础查询</param>
  7. /// <param name="OffsetPage">分页信息</param>
  8. public abstract record OffsetPagedQueryBuilder<TQueryBuilder, T>(
  9.     TQueryBuilder Query,
  10.     OffsetPageInfo? OffsetPage = null)
  11.     : IFilterPredicateBuilder<T>
  12.     , IOrderedQueryBuilder<T>
  13.     , IOffsetPagingSupport
  14.     where TQueryBuilder : IFilterPredicateBuilder<T>
  15. {
  16.     /// <inheritdoc/>
  17.     public Expression<Func<T, bool>>? GetWherePredicate() => Query.GetWherePredicate();
  18.     /// <inheritdoc/>
  19.     public virtual OffsetPageInfo OffsetPage { get; } = OffsetPage ?? new();
  20.     /// <inheritdoc/>
  21.     public abstract IOrderedQueryable<T> ApplyOrder(IQueryable<T> query);
  22. }
  23. /// <summary>
  24. /// 查询的排序方法
  25. /// </summary>
  26. public enum QueryableOrderMethod
  27. {
  28.     /// <summary>
  29.     /// 优先升序
  30.     /// </summary>
  31.     OrderBy = 1,
  32.     /// <summary>
  33.     /// 优先降序
  34.     /// </summary>
  35.     OrderByDescending,
  36.     /// <summary>
  37.     /// 次一级升序
  38.     /// </summary>
  39.     ThenBy,
  40.     /// <summary>
  41.     /// 次一级降序
  42.     /// </summary>
  43.     ThenByDescending
  44. }
  45. /// <summary>
  46. /// 关键字排序查询构造器扩展
  47. /// </summary>
  48. public static class KeySelectorOrderQueryBuilderExtensions
  49. {
  50.     private static readonly MethodInfo _queryableOederByOfT = typeof(Queryable).GetMethods()
  51.         .Single(static m => m.Name is nameof(Queryable.OrderBy) && m.GetParameters().Length is 2);
  52.     private static readonly MethodInfo _queryableThenByOfT = typeof(Queryable).GetMethods()
  53.         .Single(static m => m.Name is nameof(Queryable.ThenBy) && m.GetParameters().Length is 2);
  54.     private static readonly MethodInfo _queryableOrderByDescendingOfT = typeof(Queryable).GetMethods()
  55.         .Single(static m => m.Name is nameof(Queryable.OrderByDescending) && m.GetParameters().Length is 2);
  56.     private static readonly MethodInfo _queryableThenByDescendingOfT = typeof(Queryable).GetMethods()
  57.         .Single(static m => m.Name is nameof(Queryable.ThenByDescending) && m.GetParameters().Length is 2);
  58.     /// <summary>
  59.     /// 对查询应用关键字排序
  60.     /// </summary>
  61.     /// <typeparam name="T">查询的元素类型</typeparam>
  62.     /// <typeparam name="TOrderKey">可用的排序关键字类型</typeparam>
  63.     /// <param name="OrderInfos">排序信息</param>
  64.     /// <param name="query">原始查询</param>
  65.     /// <returns>已排序的查询</returns>
  66.     /// <exception cref="InvalidDataException"></exception>
  67.     public static IOrderedQueryable<T> ApplyKeyedOrder<T, TOrderKey>(this IKeySelectorOrderedQueryBuilder<T, TOrderKey> OrderInfos, IQueryable<T> query)
  68.         where TOrderKey : struct, Enum
  69.     {
  70.         ArgumentNullException.ThrowIfNull(OrderInfos);
  71.         ArgumentNullException.ThrowIfNull(query);
  72.         if (OrderInfos.GetSupportedOrderKeySelectors()?.Count > 0 is false) throw new InvalidDataException($"{nameof(OrderInfos.GetSupportedOrderKeySelectors)}");
  73.         IOrderedQueryable<T> orderedQuery;
  74.         QueryableOrderMethod methodKind;
  75.         MethodInfo orderMethod;
  76.         Expression<Func<T, object?>> keySelector;
  77.         var firstOrder = OrderInfos.OrderKeys?.FirstOrDefault();
  78.         if (firstOrder is not null)
  79.         {
  80.             methodKind = firstOrder.OrderKind switch
  81.             {
  82.                 OrderKind.Asc => QueryableOrderMethod.OrderBy,
  83.                 OrderKind.Desc => QueryableOrderMethod.OrderByDescending,
  84.                 _ => throw new InvalidDataException($"{nameof(OrderKind)}"),
  85.             };
  86.             keySelector = OrderInfos.GetSupportedOrderKeySelectors()[firstOrder.Key];
  87.             orderMethod = GetQueryOrderMethod<T>(methodKind, keySelector.ReturnType);
  88.             orderedQuery = (IOrderedQueryable<T>)orderMethod.Invoke(null, [query, keySelector])!;
  89.         }
  90.         else
  91.         {
  92.             keySelector = OrderInfos.GetSupportedOrderKeySelectors().First().Value;
  93.             orderedQuery = (IOrderedQueryable<T>)(GetQueryOrderMethod<T>(QueryableOrderMethod.OrderBy, keySelector.ReturnType)
  94.                 .Invoke(null, [query, keySelector]))!;
  95.         }
  96.         foreach (var subsequentOrder in OrderInfos.OrderKeys?.Skip(1) ?? [])
  97.         {
  98.             if (subsequentOrder is not null)
  99.             {
  100.                 methodKind = subsequentOrder.OrderKind switch
  101.                 {
  102.                     OrderKind.Asc => QueryableOrderMethod.ThenBy,
  103.                     OrderKind.Desc => QueryableOrderMethod.ThenByDescending,
  104.                     _ => throw new InvalidDataException($"{nameof(OrderKind)}"),
  105.                 };
  106.                 keySelector = OrderInfos.GetSupportedOrderKeySelectors()[subsequentOrder.Key];
  107.                 orderMethod = GetQueryOrderMethod<T>(methodKind, keySelector.ReturnType);
  108.                 orderedQuery = (IOrderedQueryable<T>)orderMethod.Invoke(null, [orderedQuery, keySelector])!;
  109.             }
  110.         }
  111.         return orderedQuery;
  112.     }
  113.     private static MethodInfo GetQueryOrderMethod<T>(QueryableOrderMethod method, Type orderKeyType)
  114.     {
  115.         return method switch
  116.         {
  117.             QueryableOrderMethod.OrderBy => _queryableOederByOfT.MakeGenericMethod(typeof(T), orderKeyType),
  118.             QueryableOrderMethod.OrderByDescending => _queryableOrderByDescendingOfT.MakeGenericMethod(typeof(T), orderKeyType),
  119.             QueryableOrderMethod.ThenBy => _queryableThenByOfT.MakeGenericMethod(typeof(T), orderKeyType),
  120.             QueryableOrderMethod.ThenByDescending => _queryableThenByDescendingOfT.MakeGenericMethod(typeof(T), orderKeyType),
  121.             _ => throw new InvalidDataException($"{nameof(method)}"),
  122.         };
  123.     }
  124. }
复制代码
基础查询定义
  1. /// <summary>
  2. /// 示例1
  3. /// </summary>
  4. public class Entity1
  5. {
  6.     public int Id { get; set; }
  7.     public string? Text1 { get; set; }
  8.     public Entity2? Entity2 { get; set; }
  9.     public List<Entity3> Entities3 { get; set; } = [];
  10. }
  11. /// <summary>
  12. /// 示例2
  13. /// </summary>
  14. public class Entity2
  15. {
  16.     public int Id { get; set; }
  17.     public string? Text2 { get; set; }
  18. }
  19. /// <summary>
  20. /// 示例3
  21. /// </summary>
  22. public class Entity3
  23. {
  24.     public int Id { get; set; }
  25.     public string? Text3 { get; set; }
  26.     public Entity1? Entity1 { get; set; }
  27. }
复制代码
从示例中可以看出,只必要针对数据类型的基础数据利用标量过滤器类型实现基础筛选,对于引用的其他数据类型,可以直接复用引用类型的查询天生器,并利用由组合查询天生接口提供的组合方法即可主动把复杂类型的筛选条件嵌套到当前类型的属性上。
分页查询
  1. /// <summary>
  2. /// Entity1查询生成器
  3. /// </summary>
  4. /// <param name="Text1">Entity1文本</param>
  5. /// <param name="Id">Entity1的Id</param>
  6. /// <param name="Entity2">Entity1的Entity2筛选</param>
  7. /// <param name="Entities3">Entity1的Entity3集合筛选</param>
  8. /// <param name="Reverse">是否反转条件</param>
  9. /// <inheritdoc cref="QueryBuilderBase{T}"/>
  10. /// <remarks>
  11. /// 反转条件由<see cref="QueryBuilderBase{T}"/>在<see cref="QueryBuilderBase{T}.GetWherePredicate()"/>中自动进行,此处不需要处理。
  12. /// </remarks>
  13. public sealed record Entity1QueryBuilder(
  14.     NumberSearchFilter<int>? Id = null,
  15.     StringSearchFilter? Text1 = null,
  16.     Entity2QueryBuilder? Entity2 = null,
  17.     CollectionMemberSearchFilter<Entity3QueryBuilder, Entity3>? Entities3 = null,
  18.     [EnumDataType(typeof(PredicateCombineKind))]
  19.     PredicateCombineKind? CombineType = PredicateCombineKind.And,
  20.     bool Reverse = false)
  21.     : QueryBuilderBase<Entity1>(CombineType)
  22.     , IPredicateReversible
  23. {
  24.     /// <inheritdoc/>
  25.     protected override Expression<Func<Entity1, bool>>? BuildWherePredicate()
  26.     {
  27.         List<Expression<Func<Entity1, bool>>> predicates = [];
  28.         predicates.AddIfNotNull(Id?.GetWherePredicate<Entity1>(e1 => e1.Id));
  29.         predicates.AddIfNotNull(Text1?.GetWherePredicate<Entity1>(e1 => e1.Text1!));
  30.         predicates.AddIfNotNull(Entity2?.GetWherePredicate<Entity1>(e1 => e1.Entity2!));
  31.         predicates.AddIfNotNull(Entities3?.GetWherePredicate<Entity1>(e1 => e1.Entities3));
  32.         predicates.AddIfNotNull(base.BuildWherePredicate());
  33.         var where = CombinePredicates(predicates);
  34.         return where;
  35.     }
  36. }
  37. /// <summary>
  38. /// Entity2查询生成器
  39. /// </summary>
  40. /// <param name="Text2">Entity2文本</param>
  41. /// <param name="Id">Entity2的Id</param>
  42. /// <param name="Reverse">是否反转条件</param>
  43. /// <inheritdoc cref="QueryBuilderBase{T}"/>
  44. public sealed record Entity2QueryBuilder(
  45.     NumberSearchFilter<int>? Id = null,
  46.     StringSearchFilter? Text2 = null,
  47.     [EnumDataType(typeof(PredicateCombineKind))]
  48.     PredicateCombineKind? CombineType = PredicateCombineKind.And,
  49.     bool Reverse = false)
  50.     : QueryBuilderBase<Entity2>(CombineType)
  51.     , IPredicateReversible
  52. {
  53.     /// <inheritdoc/>
  54.     protected override Expression<Func<Entity2, bool>>? BuildWherePredicate()
  55.     {
  56.         List<Expression<Func<Entity2, bool>>> predicates = [];
  57.         predicates.AddIfNotNull(Id?.GetWherePredicate<Entity2>(e1 => e1.Id));
  58.         predicates.AddIfNotNull(Text2?.GetWherePredicate<Entity2>(e2 => e2.Text2!));
  59.         predicates.AddIfNotNull(base.BuildWherePredicate());
  60.         var where = CombinePredicates(predicates);
  61.         return where;
  62.     }
  63. }
  64. /// <summary>
  65. /// Entity3查询生成器
  66. /// </summary>
  67. /// <param name="Text3">Entity3文本</param>
  68. /// <param name="Entity1">Entity3的Entity1筛选</param>
  69. /// <param name="Id">Entity3的Id</param>
  70. /// <param name="Reverse">是否反转条件</param>
  71. /// <inheritdoc cref="QueryBuilderBase{T}"/>
  72. public sealed record Entity3QueryBuilder(
  73.     NumberSearchFilter<int>? Id = null,
  74.     StringSearchFilter? Text3 = null,
  75.     Entity1QueryBuilder? Entity1 = null,
  76.     [EnumDataType(typeof(PredicateCombineKind))]
  77.     PredicateCombineKind? CombineType = PredicateCombineKind.And,
  78.     bool Reverse = false)
  79.     : QueryBuilderBase<Entity3>(CombineType)
  80.     , IPredicateReversible
  81. {
  82.     /// <inheritdoc/>
  83.     protected override Expression<Func<Entity3, bool>>? BuildWherePredicate()
  84.     {
  85.         List<Expression<Func<Entity3, bool>>> predicates = [];
  86.         predicates.AddIfNotNull(Id?.GetWherePredicate<Entity3>(e3 => e3.Id));
  87.         predicates.AddIfNotNull(Text3?.GetWherePredicate<Entity3>(e3 => e3.Text3!));
  88.         predicates.AddIfNotNull(Entity1?.GetWherePredicate<Entity3>(e3 => e3.Entity1!));
  89.         predicates.AddIfNotNull(base.BuildWherePredicate());
  90.         var where = CombinePredicates(predicates);
  91.         return where;
  92.     }
  93. }
  94. public static class CollectionExtensions
  95. {
  96.     /// <summary>
  97.     /// 如果<paramref name="item"/>不是<see langword="null"/>,把<paramref name="item"/>添加到<paramref name="collection"/>。
  98.     /// </summary>
  99.     /// <typeparam name="T">集合的元素类型。</typeparam>
  100.     /// <param name="collection">待添加元素的集合。</param>
  101.     /// <param name="item">要添加的元素。</param>
  102.     /// <returns>是否成功把元素添加到集合。</returns>
  103.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  104.     public static bool AddIfNotNull<T>(this ICollection<T> collection, T? item)
  105.     {
  106.         ArgumentNullException.ThrowIfNull(collection, nameof(collection));
  107.         if (item is not null)
  108.         {
  109.             collection.Add(item);
  110.             return true;
  111.         }
  112.         return false;
  113.     }
  114. }
复制代码
分页查询只要利用之前定义好的泛型基类填充类型参数即可。由于实例类型必要实现关键字排序,因此要实现干系接口。
高级分页查询
  1. /// <summary>
  2. /// Entity1分页查询生成器
  3. /// </summary>
  4. /// <inheritdoc cref="OffsetPagedQueryBuilder{TQueryBuilder, T2}"/>
  5. public sealed record OffsetPagedEntity1QueryBuilder(
  6.     Entity1QueryBuilder Query,
  7.     OffsetPageInfo? OffsetPage = null,
  8.     ImmutableList<OrderInfo<Entity1OrderKey>>? OrderKeys = null)
  9.     : OffsetPagedQueryBuilder<Entity1QueryBuilder, Entity1>(Query, OffsetPage)
  10.     , IKeySelectorOrderedQueryBuilder<Entity1, Entity1OrderKey>
  11. {
  12.     /// <inheritdoc/>
  13.     public IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> GetSupportedOrderKeySelectors() => Entity1OrderKeySelector.Content;
  14.     /// <inheritdoc/>
  15.     public override IOrderedQueryable<Entity1> ApplyOrder(IQueryable<Entity1> query) => this.ApplyKeyedOrder(query);
  16. }
  17. /// <summary>
  18. /// Entity1排序关键字
  19. /// </summary>
  20. public enum Entity1OrderKey : uint
  21. {
  22.     /// <summary>
  23.     /// Id
  24.     /// </summary>
  25.     Id = 1,
  26.     /// <summary>
  27.     /// Text1
  28.     /// </summary>
  29.     Text1
  30. }
  31. internal static class Entity1OrderKeySelector
  32. {
  33.     public static IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> Content { get; } =
  34.         FrozenDictionary.ToFrozenDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>>([
  35.             new(Entity1OrderKey.Id, e1 => e1.Id),
  36.             new(Entity1OrderKey.Text1, e1 => e1.Text1),
  37.         ]);
  38. }
复制代码
高级分页查询同样只必要填充预定义泛型基类的类型参数即可。
实例化查询天生器对象
  1. /// <summary>
  2. /// Entity1分页高级查询生成器
  3. /// </summary>
  4. /// <param name="OrderKeys">排序信息</param>
  5. /// <inheritdoc cref="OffsetPagedQueryBuilder{TQueryBuilder, T}"/>
  6. public sealed record OffsetPagedAdvancedEntity1QueryBuilder(
  7.     AdvancedQueryBuilder<Entity1QueryBuilder, Entity1> Query,
  8.     ImmutableList<OrderInfo<Entity1OrderKey>>? OrderKeys = null,
  9.     OffsetPageInfo? OffsetPage = null)
  10.     : OffsetPagedQueryBuilder<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>, Entity1>(
  11.         Query,
  12.         OffsetPage)
  13.     , IKeySelectorOrderedQueryBuilder<Entity1, Entity1OrderKey>
  14. {
  15.     /// <inheritdoc/>
  16.     public IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> GetSupportedOrderKeySelectors() => Entity1OrderKeySelector.Content;
  17.     /// <inheritdoc/>
  18.     public override IOrderedQueryable<Entity1> ApplyOrder(IQueryable<Entity1> query) => this.ApplyKeyedOrder(query);
  19. }
复制代码
在集合中利用天生器
  1. OffsetPagedAdvancedEntity1QueryBuilder queryBuilder =
  2.     new OffsetPagedAdvancedEntity1QueryBuilder(
  3.         Query: new AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>(
  4.             Queries: [ // ImmutableList<Entity1QueryBuilder>
  5.                 new Entity1QueryBuilder (
  6.                     Id: new NumberSearchFilter<int>([2], ComparableNumberSearchOperator.GreaterThan),
  7.                     Text1: new StringSearchFilter(["aa"], StringSearchOperator.Contains),
  8.                     Entity2: new Entity2QueryBuilder(
  9.                         new NumberSearchFilter<int>([100]),
  10.                         new StringSearchFilter(["ccc"])
  11.                     ),
  12.                     Entities3: null,
  13.                     CombineType: PredicateCombineKind.Or,
  14.                     Reverse: false
  15.                 ),
  16.                 new Entity1QueryBuilder(
  17.                     Id: new NumberSearchFilter<int>([5], ComparableNumberSearchOperator.LessThan)
  18.                 )
  19.             ],
  20.             QueryGroups: [ // ImmutableList<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>>
  21.                 new AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>(
  22.                     Queries: [ // ImmutableList<Entity1QueryBuilder>
  23.                         new Entity1QueryBuilder(
  24.                             Id: new NumberSearchFilter<int>([20], ComparableNumberSearchOperator.Equal),
  25.                             Text1: new StringSearchFilter(["bb"], StringSearchOperator.Contains),
  26.                             Entity2: null,
  27.                             Entities3: new CollectionMemberSearchFilter<Entity3QueryBuilder, Entity3>(
  28.                                 query: new Entity3QueryBuilder(
  29.                                     Id: null,
  30.                                     Text3: new StringSearchFilter(["fff"], StringSearchOperator.StartsWith)
  31.                                 ),
  32.                                 count: new NumberSearchFilter < int >([50]),
  33.                                 percent: null,
  34.                                 reverse: false
  35.                             ),
  36.                             CombineType: PredicateCombineKind.And,
  37.                             Reverse: false
  38.                         )
  39.                     ],
  40.                     QueryGroups:[ // ImmutableList<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>>
  41.                     ],
  42.                     CombineType: PredicateCombineKind.Or,
  43.                     Reverse: true
  44.                 )
  45.             ],
  46.             CombineType: PredicateCombineKind.And,
  47.             Reverse: true
  48.         ),
  49.         OrderKeys: [ // ImmutableList<OrderInfo<Entity1OrderKey>>
  50.             new OrderInfo<Entity1OrderKey>(Entity1OrderKey.Text1, OrderKind.Desc),
  51.             new OrderInfo<Entity1OrderKey>(Entity1OrderKey.Id),
  52.         ],
  53.         OffsetPage: new OffsetPageInfo(1,20)
  54.     );
复制代码
最终得到的筛选表达式
  1. // 准备一个集合
  2. var entity1Arr = new Entity1[
  3.         new()
  4.     ];
  5. // 从生成器中获取筛选表达式
  6. var where = queryBuilder.GetWherePredicate();
  7. // 把集合转换为 IQueryable<Entity1> 使用表达式类型的参数
  8. var query = entity1Arr.AsQueryable().Where(where!);
  9. // 把排序应用到查询
  10. var ordered = builder.ApplyOrder(query);
  11. // 把分页应用到查询
  12. var paged = builder.OffsetPage(ordered);
  13. // 把表达式编译为委托
  14. var whereFunc = where.Compile();
复制代码
这个表达式是经过手动去除多余的括号,重新整理缩进后得到的版本。原始表达式像这样:

在此也保举这个好用的VS插件:ReadableExpressions.Visualizers。这个插件可以把表达式显示成代码编辑器里的样子,对各种语法要素也会着色,调试动态拼接的表达式时非常好用。
EF Core天生的SQL(SQL Server)
  1. Expression<Func<Entity1, bool>> exp = e1 =>
  2. !(
  3.     (
  4.         e1.Id > 2
  5.         || e1.Text1.Contains("aa")
  6.         || (e1.Entity2.Id == 100 && e1.Entity2.Text2.Contains("ccc"))
  7.     )
  8.     && e1.Id < 5
  9.     && !(
  10.         e1.Id == 20
  11.         && e1.Text1.Contains("bb")
  12.         && e1.Entities3.AsQueryable().Count() == 50
  13.     )
  14. );
复制代码
等价的JSON表示
  1. DECLARE @__p_0 int = 0;
  2. DECLARE @__p_1 int = 20;
  3. SELECT [e].[Id], [e].[Entity2Id], [e].[Text1]
  4. FROM [Entity1] AS [e]
  5. LEFT JOIN [Entity2] AS [e0] ON [e].[Entity2Id] = [e0].[Id]
  6. WHERE ([e].[Id] <= 2 AND ([e].[Text1] NOT LIKE N'%aa%' OR [e].[Text1] IS NULL) AND ([e0].[Id] <> 100 OR [e0].[Id] IS NULL OR [e0].[Text2] NOT LIKE N'%ccc%' OR [e0].[Text2] IS NULL)) OR [e].[Id] >= 5 OR ([e].[Id] = 20 AND [e].[Text1] LIKE N'%bb%' AND (
  7.     SELECT COUNT(*)
  8.     FROM [Entity3] AS [e1]
  9.     WHERE [e].[Id] = [e1].[Entity1Id]) = 50)
  10. ORDER BY [e].[Text1] DESC, [e].[Id]
  11. OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
复制代码
JSON中的属性如果是类型定义时的默认值,可以省略不写。比方字符串搜刮的默认操纵是包含子串,条件反转的默认值是false等。
特点总结

这套查询天生器是一个完全可组合的结构。从一组内置基础类型的筛选器开始组合出基层自定义类型的天生器,再通过基础筛选器和自定义天生器的组合继续组合出具有嵌套结构的类型的筛选器,末了通过泛型的分页和高级查询天生器组合出完整的查询天生器。这些查询天生器也可以独立利用,自由度很高。比方分页查询的总数计算,就可以只提取其中的筛选表达式部分来用,其中的各种自定义筛选器也都可以当作顶层筛选器来用。
基础类型的筛选器只实现组合天生器接口,因为基础类型一定是作为其他类型的属性来用的,所以针对基础类型的条件也一定要嫁接到一个属性访问表达式上才有意义。对于自定义表达式天生器,当作为顶级类型来利用时,体现为直接天生器,以当前类型为目标天生表达式;当作为其他类型的属性时,又体现为组合天生器,把天生的条件嫁接到上层对象的属性上。
筛选器的各个属性名和作用目标属性名完全无关,这样既隔离了内部代码和外部查询,使两边互不干扰,也能轻松对外部查询隐藏内部名称,降低安全风险。由于每个可查询的属性都是明确定义的,因此完全不存在恶意攻击的可能性,如果想对参数的范围之类的信息进行审查,结构化的查询数据也非常容易操纵。从查询天生的定义中可以看出,查询的每一个片断都是静态表达式,因此天生器的所有部分都完全兼容静态编译检查和主动重构。
这个查询天生器解决了System.Linq.Dynamic.Core和LinqKit的劣势,相比较可能唯一的不便之处是代码量稍大,等价的JSON表示内容量较大,但是就因此获得的组合灵活性、序列化传输兼容性和静态安全性而言,这点代价还是可以担当的。
为了淘汰复杂查询的必要,笔者把查询关键字筹划为数组类型,再根据操纵检查具体数据。比方候选项查询,如果不直接支持,就只能利用高级查询天生器的基础天生器数组之间的Or连接来模拟。既然候选项查询必须利用数组型关键字,干脆充分利用这个数组的特点,直接提供区间查询,多个不连续区间查询等功能,最大程度淘汰对高级查询天生器的依靠,尽可能在简单查询天生器里实现绝大部分常见条件。
如果直接把表达式编译成委托来用的话,可能会出现空引用异常,因为表达式不支持空传播运算符,只能直接访问。用EF Core天生SQL不会出现问题。
结语

好久以前笔者就思考过,利用LINQ实现动态表达式天生应该怎么办。刚开始发现JqGrid这个表格组件支持嵌套的复杂条件,并以嵌套的JSON结构来表示。后来又赞叹于了HotChocolate的主动条件参数天生和架构修改配置。开始思考动态天生的问题后又先后研究了System.Linq.Dynamic.Core和LinqKit等方案,分析总结了他们的特点和优劣。几经周折终于实现了这个比较满意表达式天生器。
像平凡表达式天生器接口和组合表达式天生器接口就是研究过程中发现应该是两个差别的功能和接谈锋分离出来的。对于基础类型天生器,一定要嫁接到到其他类型的属性上才有效。而对于分页天生器来说又没有可组合的必要,要分页就说明应该是以顶级类型的身份来用。对于自定义类型的天生器来说又是两种都有可能。这样随着研究的深入问题逐步清晰的环境经常出现,而且构思阶段很难发现。
最开始分页天生器是没有通用泛型类的,必要自己继承,但是用了一段时间发现这个东西形态固定,实际上可以用泛型类实现。主动化条件反转和防止重复反转也是后来才发现息争决。
这次研究能顺利进行下去的一个关键是想到了对于复杂嵌套类型,可以把完整的条件表达式拆分为从顶级类型到目标类型的访问表达式和针对目标类型的条件表达式作为两个独立的部分来处理,然后利用表达式访问器拼合两个部分。这样使得天生器和数据类型一样可以自由组合。嵌套的表达式天生问题曾一直困扰着笔者,直到弄懂了表达式访问器的用法和打通了思路。
经过这次研究,对表达式的利用也更加熟练,收获颇丰。接待园友体验交流。
QQ群

读者交流QQ群:540719365

接待读者和广大朋侪一起交流,如发现本书错误也接待通过博客园、QQ群等方式告知笔者。
本文地址:如安在 .NET 中构建一个好用的动态查询天生器

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表