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

前言

自从.NET Framework 3.5提供了LINQ之后,集合数据查询基本被LINQ统一了。这大幅提高了编写数据查询代码的效率和质量,但是在需要编写动态查询的时候反而很困难,特别是最常用的where和order by子句,他们的参数是Expression。编写静态查询的时候编译器会自动把代码转换成等价的表达式,而动态查询无法借助编译器完成表达式构建,只能手动拼接。想要正确拼接一个描述低级代码结构的表达式对开发者的功力提出了较高的要求,哪怕是这方面的高手也容易翻车。

为了简化查询表达式的动态构建,社区出现了很多表达式生成辅助库。其中最知名当属System.Linq.Dynamic.CoreLinqKitSystem.Linq.Dynamic.Core使用字符串定义表达式,并在内部转换成ExpressionLinqKit则是使用PredicateBuilder<T>把复杂表达式拆分成多个片段的组合。但是他们也存在一些不便之处,System.Linq.Dynamic.Core牺牲了代码的静态检查能力,只有在运行时才知道表达式是否正确。如果把表达式作为允许前端填写的参数,不仅需要让前端开发人员多学习一套表达式定义语法,还会产生安全漏洞。如果想提前检查表达式的安全性,就需要对字符串进行分析。分析字符串生成表达式会成为一个流行库的原因之一就是分析这个字符串很难,这样一来相当于把外包出去的困难任务又拿回来了。LinqKit则是对前端不友好,这种类型无法序列化传输,如果想通过前端配合使用,还是需要再想办法写一套转换代码和配套的可序列化数据结构。

这两个库在传输序列化和动态拼接简化方面各有显著优势,也各有明显不足。因此笔者开始思考是否有办法开发一个便于序列化传输,安全性能得到静态检查保证,对于复杂表达式的拼接也能良好支持的表达式生成器。经过多次摸索,终有一些心得,在此分享给大家。

新书宣传

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

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

正文

提炼基本概念

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

想要保证构建的查询安全性,势必要限制能够查询的属性,最好能让生成器中只出现可以查询的属性。为了避免底层属性改名导致查询出错,或是隐藏代码中的属性名,暴露给生成器的名称应该和真实属性名解耦,两者能独立调整。对于查询器中可能出现的特殊自定义条件提供自定义扩展点。最好支持静态编译检查和基于自动重构的自动代码调整。

csharp 复制代码
/// <summary>
/// 查询条件构造器接口
/// </summary>
public interface IFilterPredicateBuilder<T>
{
    /// <summary>
    /// 获取查询条件
    /// </summary>
    /// <returns>生成的查询条件</returns>
    Expression<Func<T, bool>>? GetWherePredicate();
}

基于以上假设,笔者提炼出了这个基本接口,用于生成器表示支持生成谓词表达式。接口没有任何额外内容以允许最大程度的自定义扩展。

为复杂的嵌套查询提供支持

一个完备的表达式生成一定会面临嵌套对象属性的情况,这其中的问题在于,对象类型无穷无尽,相同类型的对象也可能出现在各种地方。如何访问到需要的对象属性并应用筛选条件就是一个需要仔细考虑的问题。在笔者看来,这个问题可以分解为两个子问题,访问属性和应用条件。将这两个部分分离开,条件就可以只针对最终类型开发,属性的访问则交由外部决定。这样一来,针对某种类型开发的条件就可以在任何地方的属性上使用。

csharp 复制代码
/// <summary>
/// 可组合的查询条件构造器接口
/// </summary>
public interface IComposableFilterPredicateBuilder<T>
{
    /// <summary>
    /// 获取查询条件,并把条件应用到<typeparamref name="TOwner"/>类型的对象所拥有的<typeparamref name="T"/>类型的成员上。
    /// </summary>
    /// <typeparam name="TOwner">拥有<typeparamref name="T"/>类型的成员的类型</typeparam>
    /// <param name="memberAccesser">成员访问器</param>
    /// <returns>已应用到成员的查询条件</returns>
    Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccesser);
}

/// <summary>
/// 值类型可组合的查询条件构造器接口
/// </summary>
public interface IStructComposableFilterPredicateBuilder<T> : IComposableFilterPredicateBuilder<T>
    where T : struct
{
    /// <summary>
    /// 获取查询条件,并把条件应用到<typeparamref name="TOwner"/>类型的对象所拥有的<typeparamref name="T"/>类型的成员上。
    /// </summary>
    /// <typeparam name="TOwner">拥有<typeparamref name="T"/>类型的成员的类型</typeparam>
    /// <param name="memberAccesser">成员访问器</param>
    /// <returns>已应用到成员的查询条件</returns>
    Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T?>> memberAccesser);
}

基于以上假设,可以再次提炼出一个接口。通过参数由外部决定属性如何访问,并返回最终拼合条件。值类型需要特殊处理。

为集合类型的查询提供支持

有时要查询的属性可能是集合类型,这种查询和普通的单值查询有区别,需要单独处理。

csharp 复制代码
/// <summary>
/// 集合可组合的查询条件构造器接口
/// </summary>
public interface ICollectionComposableFilterPredicateBuilder<T>
{
    /// <summary>
    /// 获取查询条件,并把条件应用到<typeparamref name="TOwner"/>类型的对象所拥有的<typeparamref name="T"/>类型的集合的成员上。
    /// </summary>
    /// <typeparam name="TOwner">拥有<typeparamref name="T"/>类型的集合的成员的类型</typeparam>
    /// <param name="memberAccesser">成员访问器</param>
    /// <returns>已应用到成员的查询条件</returns>
    Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, IEnumerable<T>>> memberAccesser);
}

这表示专门用于集合类型的查询,IQueryable<T>实现了IEnumerable<T>,不需要单独定义。

条件反转

一键支持条件反转是个非常有用的功能,如果一个条件有多个子条件,且条件之间混合了各种加了括号的且或非连接,想要正确反转这样条件非常容易困难。

csharp 复制代码
/// <summary>
/// 可反转条件接口
/// </summary>
public interface IPredicateReversible
{
    /// <summary>
    /// 是否反转条件
    /// </summary>
    bool Reverse { get; }
}

使用一个bool标记反转条件,由查询生成器自动处理反转是合理的选择。

序列化传输支持

到此为止,一个完备的表达式生成器所需的基本接口就提炼完成了。但是这些接口所表达的概念并不支持序列化传输,接下来就要解决这问题。

序列化传输查询条件意味着要分离出条件中可以序列化的部分。例如:Foo.Bar > 1,此处需要传输的部分是属性,比较方式,比较参数。属性的话由于需要支持静态检查,需要单独处理。对于比较方式,办法比较多,笔者选择使用枚举来表达。关键字一般是各种基础类型,应该天然支持序列化。

csharp 复制代码
/// <summary>
/// 引用类型搜索关键字接口
/// </summary>
/// <typeparam name="T">关键字类型</typeparam>
public interface ISearchFilterClassKey<T> where T : class
{
    /// <summary>
    /// 搜索关键字
    /// </summary>
    ImmutableList<T?> Keys { get; }
}

/// <summary>
/// 值类型搜索关键字接口
/// </summary>
/// <typeparam name="T">关键字类型</typeparam>
public interface ISearchFilterStructKey<T> where T : struct
{
    /// <summary>
    /// 搜索关键字
    /// </summary>
    ImmutableList<T?> Keys { get; }
}

/// <summary>
/// 搜索操作符接口
/// </summary>
/// <typeparam name="TOperator"></typeparam>
public interface ISearchFilterOperator<TOperator> where TOperator : struct, Enum
{
    /// <summary>
    /// 搜索操作符
    /// </summary>
    TOperator Operator { get; }
}

基于以上假设,可以提炼出以上接口。

为概念接口提供实现

这些接口表达了查询生成器所需的各种概念,但是让开发者自行实现并不是好主意,这些接口对于开发者来说应该是用做泛型约束的。笔者势必要为此提供一套最常见情形的实现。

csharp 复制代码
/// <summary>
/// 查询构造器基类
/// </summary>
/// <typeparam name="T">查询的数据类型</typeparam>
/// <param name="CombineType">条件谓词组合方式。Json属性名用 combine 减少字数。</param>
/// <inheritdoc cref="IFilterPredicateBuilder{T}"/>
public abstract record QueryBuilderBase<T>(
    [EnumDataType(typeof(PredicateCombineKind))]
    PredicateCombineKind? CombineType = PredicateCombineKind.And)
    : IFilterPredicateBuilder<T>, IComposableFilterPredicateBuilder<T>
{
    private static readonly MethodInfo _logicallyDeletePredicateOfT = typeof(AuditableQueryPredicateExtensions)
        .GetMethod(
            nameof(AuditableQueryPredicateExtensions.GetLogicallyDeleteQueryPredicate),
            BindingFlags.Public | BindingFlags.Static
        )!;

    private static readonly MethodInfo _DependencylogicallyDeletePredicateOfT = typeof(AuditableQueryPredicateExtensions)
        .GetMethod(
            nameof(AuditableQueryPredicateExtensions.GetDependencyLogicallyDeleteQueryPredicate),
            BindingFlags.Public | BindingFlags.Static
        )!;

    /// <inheritdoc/>
    public Expression<Func<T, bool>>? GetWherePredicate()
    {
        var where = BuildWherePredicate();
        if (this is IPredicateReversible reversible) where = reversible.ApplyReversiblePredicate(where);
        return where;
    }

    /// <summary>
    /// 构造查询条件
    /// </summary>
    /// <returns>获得的查询条件</returns>
    /// <remarks>
    /// 派生类重写时请只负责构造自身的条件,
    /// 最后使用<see cref="CombinePredicates"/>合并来自基类的条件后再返回。
    /// 不要在这里进行条件反转。
    /// </remarks>
    protected virtual Expression<Func<T, bool>>? BuildWherePredicate()
    {
        return null;
    }

    /// <summary>
    /// 组合查询条件
    /// </summary>
    /// <param name="predicates">待组合的子条件</param>
    /// <returns></returns>
    /// <exception cref="NotSupportedException"></exception>
    protected Expression<Func<T, bool>>? CombinePredicates(IEnumerable<Expression<Func<T, bool>>>? predicates)
    {
        var predicate = predicates?.FirstOrDefault();
        if (predicates?.Any() is true)
        {
            predicate = CombineType switch
            {
                PredicateCombineKind.And => predicates?.AndAlsoAll(),
                PredicateCombineKind.Or => predicates?.OrElseAll(),
                _ => throw new NotSupportedException(CombineType.ToString()),
            };
        }

        return predicate;
    }

    /// <inheritdoc/>
    /// <exception cref="ArgumentNullException"></exception>
    public Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccesser)
    {
        ArgumentNullException.ThrowIfNull(memberAccesser);

        var where = GetWherePredicate();
        if (where is null) return null;

        MemberReplaceExpressionVisitor visitor = new();
        var result = visitor.ReplaceMember(memberAccesser, where);

        return result;
    }

    /// <summary>
    /// 成员访问表达式替换访问器
    /// </summary>
    private sealed class MemberReplaceExpressionVisitor : ExpressionVisitor
    {
        private readonly Lock _lock = new();

        private volatile bool _calledFromReplaceMember = false;
        private LambdaExpression? _memberAccesser;

        /// <summary>
        /// 把指定表达式的成员访问替换为新的成员访问。
        /// </summary>
        /// <typeparam name="TOwner">拥有<typeparamref name="TMember"/>类型的成员的类型</typeparam>
        /// <typeparam name="TMember">用于替换成员访问的类型</typeparam>
        /// <typeparam name="TResult">返回值类型</typeparam>
        /// <param name="memberAccessor">替换用的新成员访问表达式。</param>
        /// <param name="resultAccessor">要替换成员访问的表达式。</param>
        /// <returns>已替换成员访问的表达式。</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public Expression<Func<TOwner, TResult>> ReplaceMember<TOwner, TMember, TResult>(
            Expression<Func<TOwner, TMember>> memberAccessor,
            Expression<Func<TMember, TResult>> resultAccessor)
        {
            ArgumentNullException.ThrowIfNull(resultAccessor);
            ArgumentNullException.ThrowIfNull(memberAccessor);

            lock (_lock)
            {
                try
                {
                    _calledFromReplaceMember = true;
                    _memberAccesser = memberAccessor;

                    var newLambda = (LambdaExpression)Visit(resultAccessor);

                    return Expression.Lambda<Func<TOwner, TResult>>(newLambda.Body, memberAccessor.Parameters);
                }
                catch
                {
                    throw;
                }
                finally
                {
                    _calledFromReplaceMember = false;
                    _memberAccesser = null;
                }
            }
        }

        /// <inheritdoc/>
        [return: NotNullIfNotNull(nameof(node))]
        public override Expression? Visit(Expression? node)
        {
            if (!_calledFromReplaceMember) throw new InvalidOperationException($"Don't call directly, call {nameof(ReplaceMember)} instead.");

            return base.Visit(node);
        }

        /// <inheritdoc/>
        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression is ParameterExpression)
            {
                return Expression.PropertyOrField(_memberAccesser!.Body, node.Member.Name);
            }

            return base.VisitMember(node);
        }
    }
}

/// <summary>
/// 条件谓词组合方式
/// </summary>
public enum PredicateCombineKind
{
    /// <summary>
    /// 且
    /// </summary>
    And = 1,

    /// <summary>
    /// 或
    /// </summary>
    Or = 2
}

/// <summary>
/// 可反转谓词接口扩展
/// </summary>
public static class PredicateReversibleExtensions
{
    /// <summary>
    /// 应用可反转的谓词
    /// </summary>
    /// <typeparam name="T">谓词表达式的参数类型</typeparam>
    /// <param name="reversible">可反转谓词接口的实例</param>
    /// <param name="predicate">谓词表达式</param>
    /// <returns></returns>
    public static Expression<Func<T, bool>>? ApplyReversiblePredicate<T>(
        this IPredicateReversible reversible,
        Expression<Func<T, bool>>? predicate)
    {
        return !reversible.Reverse ? predicate : predicate?.Not();
    }
}

在接口的GetWherePredicate()方法之外,增加一个内部的BuildWherePredicate()方法,把生成基本条件和反转条件隔离开并统一处理,确保反转条件只会在最后进行一次。

实现通用的基本类型过滤器表达式

定义操作类型

csharp 复制代码
/// <summary>
/// 基本搜索操作
/// </summary>
public enum BaseSearchOperator : uint
{
    /// <summary>
    /// 等于
    /// </summary>
    Equal = 1 << 0,

    /// <summary>
    /// 是候选项之一
    /// </summary>
    In = 1 << 1,
}

/// <summary>
/// 字符串搜索操作
/// </summary>
public enum StringSearchOperator : uint
{
    /// <summary>
    /// 等于
    /// </summary>
    Equal = 1 << 0,

    /// <summary>
    /// 是候选项之一
    /// </summary>
    In = 1 << 1,

    /// <summary>
    /// 包含
    /// </summary>
    Contains = 1 << 2,

    /// <summary>
    /// 包含全部候选项
    /// </summary>
    EqualContains = Equal | Contains,

    /// <summary>
    /// 包含候选项之一
    /// </summary>
    InContains = In | Contains,

    /// <summary>
    /// 开头是
    /// </summary>
    StartsWith = 1 << 3,

    /// <summary>
    /// 开头是候选项之一
    /// </summary>
    InStartsWith = In | StartsWith,

    /// <summary>
    /// 结尾是
    /// </summary>
    EndsWith = 1 << 4,

    /// <summary>
    /// 结尾是候选项之一
    /// </summary>
    InEndsWith = In | EndsWith,
}

/// <summary>
/// 可排序数字搜索操作
/// </summary>
public enum ComparableNumberSearchOperator : uint
{
    /// <summary>
    /// 等于
    /// </summary>
    Equal = 1 << 0,

    /// <summary>
    /// 是候选项之一
    /// </summary>
    In = 1 << 1,

    /// <summary>
    /// 小于
    /// </summary>
    LessThan = 1 << 2,

    /// <summary>
    /// 小于等于
    /// </summary>
    LessThanOrEqual = LessThan | Equal,

    /// <summary>
    /// 大于
    /// </summary>
    GreaterThan = 1 << 3,

    /// <summary>
    /// 大于等于
    /// </summary>
    GreaterThanOrEqual = GreaterThan | Equal,

    /// <summary>
    /// 介于两个值之间,但不包含两边的边界值
    /// </summary>
    BetweenOpen = 1 << 4,

    /// <summary>
    /// 是多组介于两个值之间,但不包含两边的边界值的候选区间之一
    /// </summary>
    InBetweenOpen = In | BetweenOpen,

    /// <summary>
    /// 介于两个值之间,包含左边界值,但不包含右边界值
    /// </summary>
    BetweenLeftClosed = 1 << 5,

    /// <summary>
    /// 是多组介于两个值之间,包含左边界值,但不包含右边界值的候选区间之一
    /// </summary>
    InBetweenLeftClosed = In | BetweenLeftClosed,

    /// <summary>
    /// 介于两个值之间,包含右边界值,但不包含左边界值
    /// </summary>
    BetweenRightClosed = 1 << 6,

    /// <summary>
    /// 是多组介于两个值之间,包含右边界值,但不包含左边界值的候选区间之一
    /// </summary>
    InBetweenRightClosed = In | BetweenRightClosed,

    /// <summary>
    /// 介于两个值之间,同时包含两边的边界值
    /// </summary>
    BetweenClosed = BetweenOpen | BetweenLeftClosed | BetweenRightClosed,

    /// <summary>
    /// 是多组介于两个值之间,同时包含两边的边界值的候选区间之一
    /// </summary>
    InBetweenClosed = In | BetweenClosed,
}

类似不等于这种操作使用等于和反转条件的组合来表示。同时这些操作使用位枚举让每个位都能用于表达操作所具有的特征。

具体实现

csharp 复制代码
/// <summary>
/// 值类型基本搜索过滤器
/// </summary>
/// <typeparam name="T">要搜索的值类型</typeparam>
public record StructSearchFilter<T>
    : ISearchFilterStructKey<T>
    , ISearchFilterOperator<BaseSearchOperator>
    , IStructComposableFilterPredicateBuilder<T>
    , IPredicateReversible
    where T : struct
{
    private static readonly Type _baseType = typeof(T);
    private static readonly Type _nullableType = typeof(T?);

    private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
        .GetMethods()
        .Where(static m => m.Name is nameof(Enumerable.Contains))
        .Single(static m => m.GetParameters().Length is 2)
        .MakeGenericMethod([_baseType]);

    private static readonly MethodInfo _enumerableNullableContains = typeof(Enumerable)
        .GetMethods()
        .Where(static m => m.Name is nameof(Enumerable.Contains))
        .Single(static m => m.GetParameters().Length is 2)
        .MakeGenericMethod([_nullableType]);

    /// <summary>
    /// 初始化一个新实例
    /// </summary>
    /// <param name="keys">搜索关键字</param>
    /// <param name="operator">搜索操作符</param>
    /// <param name="reverse">是否反转条件</param>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="InvalidEnumArgumentException"></exception>
    public StructSearchFilter(
        ImmutableList<T?> keys,
        [EnumDataType(typeof(BaseSearchOperator))]
        BaseSearchOperator @operator = BaseSearchOperator.Equal,
        bool reverse = false)
    {
        ArgumentNullException.ThrowIfNull(nameof(keys));
        if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
        if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());

        if (@operator is BaseSearchOperator.In && keys is null or { Count: < 2 })
        {
            throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时必须设置多个元素。", nameof(keys));
        }
        else if (@operator is not BaseSearchOperator.In && keys is { Count: > 1 })
        {
            throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时必须设置一个元素。", nameof(keys));
        }
        else if (@operator is not (BaseSearchOperator.In or BaseSearchOperator.Equal) && keys.Any(static n => Equals(n, null)))
        {
            throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时元素的值不能为空。", nameof(keys));
        }

        Keys = keys;
        Operator = @operator;
        Reverse = reverse;
    }

    /// <inheritdoc/>
    public virtual ImmutableList<T?> Keys { get; }

    /// <inheritdoc/>
    public virtual BaseSearchOperator Operator { get; }

    /// <inheritdoc/>
    public virtual bool Reverse { get; }

    /// <inheritdoc/>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="InvalidDataException"></exception>
    public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccessor)
    {
        if (Keys.Any(static n => n is null)) throw new InvalidOperationException("不能使用值为空的元素搜索值不能为空的成员。");

        Expression newBody = Operator switch
        {
            BaseSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(Keys.First(), _baseType)),
            BaseSearchOperator.In => Expression.Call(null, _enumerableContains, [Expression.Constant(Keys.Cast<T>().ToList(), typeof(IEnumerable<T>)), memberAccessor.Body]),
            _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
        };

        if (Reverse) newBody = Expression.Not(newBody);

        return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
    }

    /// <inheritdoc/>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="InvalidDataException"></exception>
    public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, T?>> memberAccessor)
    {
        Expression newBody = Operator switch
        {
            BaseSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(Keys.First(), _nullableType)),
            BaseSearchOperator.In => Expression.Call(null, _enumerableNullableContains, [Expression.Constant(Keys, typeof(IEnumerable<T?>)), memberAccessor.Body]),
            _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
        };

        if (Reverse) newBody = Expression.Not(newBody);

        return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
    }
}

/// <summary>
/// 布尔搜索过滤器
/// </summary>
public record BoolSearchFilter : StructSearchFilter<bool>
{
    /// <inheritdoc/>
    public BoolSearchFilter(
        ImmutableList<bool?> keys,
        [EnumDataType(typeof(BaseSearchOperator))]
        BaseSearchOperator @operator = BaseSearchOperator.Equal,
        bool reversePredicate = false) : base(keys, @operator, reversePredicate)
    {
    }
}

/// <summary>
/// 可排序数字搜索过滤器
/// </summary>
/// <typeparam name="TNumber">数字的类型</typeparam>
public record NumberSearchFilter<TNumber>
    : IStructComposableFilterPredicateBuilder<TNumber>
    , ISearchFilterStructKey<TNumber>
    , ISearchFilterOperator<ComparableNumberSearchOperator>
    , IPredicateReversible
    where TNumber : struct, IComparisonOperators<TNumber, TNumber, bool>
{
    /// <summary>
    /// 初始化一个新实例
    /// </summary>
    /// <param name="keys">搜索关键字</param>
    /// <param name="operator">搜索操作符</param>
    /// <param name="reverse">是否反转条件</param>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="InvalidEnumArgumentException"></exception>
    public NumberSearchFilter(
        ImmutableList<TNumber?> keys,
        [EnumDataType(typeof(ComparableNumberSearchOperator))]
        ComparableNumberSearchOperator @operator = ComparableNumberSearchOperator.Equal,
        bool reverse = false)
    {
        ArgumentNullException.ThrowIfNull(nameof(keys));
        if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
        if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());

        string? message = GetKeysCheckMessage(keys, @operator);
        if (message is not null) throw new ArgumentException(message, nameof(keys));

        Keys = keys;
        Operator = @operator;
        Reverse = reverse;
    }

    /// <inheritdoc/>
    public virtual ImmutableList<TNumber?> Keys { get; }

    /// <inheritdoc/>
    public virtual ComparableNumberSearchOperator Operator { get; }

    /// <inheritdoc/>
    public virtual bool Reverse { get; }

    /// <inheritdoc/>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="InvalidDataException"></exception>
    public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, TNumber>> memberAccessor)
    {
        NullKeyCheck(Keys);

        var where = GetWherePredicateExtension(Keys, Operator, memberAccessor);
        if (Reverse) where = where.Not();
        return where;
    }

    /// <inheritdoc/>
    /// <exception cref="InvalidOperationException"></exception>
    /// <exception cref="InvalidDataException"></exception>
    public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, TNumber?>> memberAccessor)
    {
        var where = GetWherePredicateExtension(Keys, Operator, memberAccessor);
        if (Reverse) where = where.Not();
        return where;
    }
}

/// <summary>
/// 字符串搜索过滤器
/// </summary>
public record StringSearchFilter
    : IComposableFilterPredicateBuilder<string>,
    ISearchFilterOperator<StringSearchOperator>,
    ISearchFilterClassKey<string>,
    IPredicateReversible
{
    private static readonly MethodInfo _contains = typeof(string)
        .GetMethod(
            nameof(string.Contains),
            BindingFlags.Public | BindingFlags.Instance,
            [typeof(string)]
        )!;

    private static readonly MethodInfo _startsWith = typeof(string)
        .GetMethod(
            nameof(string.StartsWith),
            BindingFlags.Public | BindingFlags.Instance,
            [typeof(string)]
        )!;

    private static readonly MethodInfo _endsWith = typeof(string)
        .GetMethod(
            nameof(string.EndsWith),
            BindingFlags.Public | BindingFlags.Instance,
            [typeof(string)]
        )!;

    private static readonly MethodInfo _equals = typeof(string)
        .GetMethod(
            nameof(string.Equals),
            BindingFlags.Public | BindingFlags.Instance,
            [typeof(string)]
        )!;

    private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
        .GetMethods()
        .Where(static m => m.Name is nameof(Enumerable.Contains))
        .Single(static m => m.GetParameters().Length is 2)
        .MakeGenericMethod([typeof(string)]);

    /// <summary>
    /// 初始化一个新实例
    /// </summary>
    /// <param name="keys">搜索关键字</param>
    /// <param name="operator">搜索操作符</param>
    /// <param name="reverse">是否反转条件</param>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="InvalidEnumArgumentException"></exception>
    public StringSearchFilter(
        ImmutableList<string?> keys,
        [EnumDataType(typeof(StringSearchOperator))]
        StringSearchOperator @operator = StringSearchOperator.Contains,
        bool reverse = false)
    {
        ArgumentNullException.ThrowIfNull(nameof(keys));
        if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
        if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());

        string? exceptionHint = null;
        switch (@operator)
        {
            case StringSearchOperator.Equal:
                if (keys is { Count: > 1 })
                {
                    exceptionHint = $"必须设置一个元素。";
                    goto default;
                }
                break;
            case StringSearchOperator.In:
                if (keys is { Count: < 2 })
                {
                    exceptionHint = $"必须设置多个元素。";
                    goto default;
                }
                break;
            case StringSearchOperator.Contains:
                goto case StringSearchOperator.Equal;
            case StringSearchOperator.EqualContains:
                if (keys is { Count: < 2 })
                {
                    exceptionHint = $"必须设置多个元素。";
                    goto default;
                }
                else if (keys.Any(static key => key is null))
                {
                    exceptionHint = $"元素不能为空。";
                    goto default;
                }
                break;
            case StringSearchOperator.InContains:
                goto case StringSearchOperator.EqualContains;
            case StringSearchOperator.StartsWith:
                if (keys is { Count: > 2 })
                {
                    exceptionHint = $"必须设置一个元素。";
                    goto default;
                }
                else if (keys.Any(static key => key is null))
                {
                    exceptionHint = $"元素不能为空。";
                    goto default;
                }
                break;
            case StringSearchOperator.InStartsWith:
                goto case StringSearchOperator.EqualContains;
            case StringSearchOperator.EndsWith:
                goto case StringSearchOperator.StartsWith;
            case StringSearchOperator.InEndsWith:
                goto case StringSearchOperator.EqualContains;
            default:
                exceptionHint ??= "的元素数量错误。";
                throw new ArgumentException($"当 {nameof(@operator)} 的值为 {@operator} 时{exceptionHint}", nameof(keys));
        };

        Keys = keys;
        Operator = @operator;
        Reverse = reverse;
    }

    /// <inheritdoc/>
    public virtual ImmutableList<string?> Keys { get; }

    /// <inheritdoc/>
    public virtual StringSearchOperator Operator { get; }

    /// <inheritdoc/>
    public virtual bool Reverse { get; }

    /// <inheritdoc/>
    /// <exception cref="InvalidDataException"></exception>
    public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, string>> memberAccessor)
    {
        (MethodInfo method, object? value, Type type) = Operator switch
        {
            StringSearchOperator.Equal => (_equals, (object?)Keys![0], typeof(string)),
            StringSearchOperator.In => (_enumerableContains, Keys, typeof(IEnumerable<string?>)),
            StringSearchOperator.Contains => (_contains, Keys![0], typeof(string)),
            StringSearchOperator.EqualContains => (_contains, Keys, typeof(string)),
            StringSearchOperator.InContains => (_contains, Keys, typeof(string)),
            StringSearchOperator.StartsWith => (_startsWith, Keys![0], typeof(string)),
            StringSearchOperator.InStartsWith => (_startsWith, Keys, typeof(string)),
            StringSearchOperator.EndsWith => (_endsWith, Keys![0], typeof(string)),
            StringSearchOperator.InEndsWith => (_endsWith, Keys, typeof(string)),
            _ => throw new InvalidDataException(nameof(StringSearchOperator))
        };

        Expression newBody;
        switch (Operator)
        {
            case StringSearchOperator.Equal:
                newBody = Expression.Call(memberAccessor.Body, method, Expression.Constant(value, type));
                break;
            case StringSearchOperator.In:
                newBody = Expression.Call(null, method, [Expression.Constant(value, type), memberAccessor.Body]);
                break;
            case StringSearchOperator.Contains:
                goto case StringSearchOperator.Equal;
            case StringSearchOperator.EqualContains:
                newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.And);
                break;
            case StringSearchOperator.InContains:
                newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.Or);
                break;
            case StringSearchOperator.StartsWith:
                goto case StringSearchOperator.Equal;
            case StringSearchOperator.InStartsWith:
                newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.Or);
                break;
            case StringSearchOperator.EndsWith:
                goto case StringSearchOperator.Equal;
            case StringSearchOperator.InEndsWith:
                goto case StringSearchOperator.InStartsWith;
            default:
                throw new InvalidDataException(nameof(StringSearchOperator));
        }

        if (Reverse) newBody = Expression.Not(newBody);

        return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);

        static Expression CombineIn(IReadOnlyList<string?> keys, Expression instance, MethodInfo method, Type type, PredicateCombineKind combineKind)
        {
            Expression expr = Expression.Call(instance, method, Expression.Constant(keys[0], type));

            foreach (var key in keys.Skip(1))
            {
                expr = combineKind switch
                {
                    PredicateCombineKind.And => Expression.AndAlso(expr, Expression.Call(instance, method, Expression.Constant(key, type))),
                    PredicateCombineKind.Or => Expression.OrElse(expr, Expression.Call(instance, method, Expression.Constant(key, type))),
                    _ => throw new NotImplementedException(),
                };
            }

            return expr;
        }
    }
}

此处展示了一部分基础数据类型的过滤器定义。如果将来有自定义基本类型也可以照葫芦画瓢。

集合类型属性的实现

csharp 复制代码
/// <summary>
/// 复杂类型的集合属性搜索过滤器
/// </summary>
/// <typeparam name="TQueryBuilder">要搜索的集合元素类型的查询类型</typeparam>
/// <typeparam name="T">要搜索的元素类型</typeparam>
public record CollectionMemberSearchFilter<TQueryBuilder, T>
    : ICollectionComposableFilterPredicateBuilder<T>
    , IPredicateReversible
    where TQueryBuilder : IFilterPredicateBuilder<T>
{
    private static readonly MethodInfo _asQueryableOfT = typeof(Queryable)
        .GetMethods()
        .Single(static m => m.Name is nameof(Queryable.AsQueryable) && m.IsGenericMethod);

    private static readonly MethodInfo _queryableWhereOfT = typeof(Queryable)
        .GetMethods()
        .Single(static m =>
        {
            return m.Name is nameof(Queryable.Where)
                && m.GetParameters()[1]
                    .ParameterType
                    .GenericTypeArguments[0]
                    .GenericTypeArguments
                    .Length is 2;
        });

    private static readonly MethodInfo _queryableCountOfT = typeof(Queryable)
        .GetMethods()
        .Single(static m => m.Name is nameof(Queryable.Count) && m.GetParameters().Length is 1);

    private static readonly MethodInfo _queryableAnyOfT = typeof(Queryable)
        .GetMethods()
        .Single(static m => m.Name is nameof(Queryable.Any) && m.GetParameters().Length is 1);

    private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
        .GetMethods()
        .Where(static m => m.Name is nameof(Enumerable.Contains))
        .Single(static m => m.GetParameters().Length is 2)
        .MakeGenericMethod([typeof(T)]);

    /// <summary>
    /// 元素的查询
    /// </summary>
    public TQueryBuilder? Query { get; }

    /// <summary>
    /// 计数搜索过滤器
    /// </summary>
    /// <remarks>和<see cref="Percent"/>只能存在一个。</remarks>
    public NumberSearchFilter<int>? Count { get; }

    /// <summary>
    /// 比例搜索过滤器
    /// </summary>
    /// <remarks>
    /// 和<see cref="Count"/>只能存在一个。
    /// 如果存在,则<see cref="Query"/>也必须同时存在。
    /// </remarks>
    public NumberSearchFilter<double>? Percent { get; }


    /// <inheritdoc/>
    public bool Reverse { get; }

    /// <summary>
    /// 初始化新的实例
    /// </summary>
    /// <param name="query">元素的查询</param>
    /// <param name="count">计数搜索过滤器</param>
    /// <exception cref="ArgumentNullException"></exception>
    public CollectionMemberSearchFilter(
        TQueryBuilder? query,
        NumberSearchFilter<int> count,
        bool reverse = false) : this(query, count, null, reverse)
    {
    }

    /// <summary>
    /// 初始化新的实例
    /// </summary>
    /// <param name="query">元素的查询</param>
    /// <param name="percent">比例搜索过滤器</param>
    /// <exception cref="ArgumentNullException"></exception>
    public CollectionMemberSearchFilter(
        TQueryBuilder query,
        NumberSearchFilter<double> percent,
        bool reverse = false) : this(query, null, percent, reverse)
    {
    }

    /// <summary>
    /// 初始化新的实例
    /// </summary>
    /// <param name="query">元素的查询</param>
    /// <param name="count">计数搜索过滤器</param>
    /// <param name="percent">比例搜索过滤器</param>
    /// <exception cref="ArgumentException"></exception>
    [JsonConstructor]
    public CollectionMemberSearchFilter(
        TQueryBuilder? query,
        NumberSearchFilter<int>? count = null,
        NumberSearchFilter<double>? percent = null,
        bool reverse = false)
    {
        if (count is null && percent is null || count is not null && percent is not null)
        {
            throw new ArgumentException($"{nameof(count)} 和 {nameof(percent)} 必须设置且只能设置其中一个。");
        }

        if (percent is not null && query is null)
        {
            throw new ArgumentException($"{nameof(percent)} 和 {nameof(query)} 必须同时设置。");
        }

        Count = count;
        Percent = percent;
        Reverse = reverse;
    }

    /// <inheritdoc/>
    /// <exception cref="ArgumentException"></exception>
    /// <exception cref="InvalidDataException"></exception>
    public Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, IEnumerable<T>>> memberAccessor)
    {
        ArgumentNullException.ThrowIfNull(memberAccessor);

        var asQueryable = _asQueryableOfT.MakeGenericMethod(typeof(T));
        Expression queryable = Expression.Call(null, asQueryable, [memberAccessor.Body]);
        Expression originalQueryable = queryable;

        var queryCount = _queryableCountOfT.MakeGenericMethod(typeof(T));
        Expression allCount = Expression.Call(null, queryCount, queryable);
        Expression? whereCount = null;

        var where = Query?.GetWherePredicate();
        if (where != null)
        {
            var queryableWhere = _queryableWhereOfT.MakeGenericMethod(typeof(T));
            queryable = Expression.Call(null, queryableWhere, [queryable, where]);

            whereCount = Expression.Call(null, queryCount, queryable);
        }

        Expression? resultBody = null;
        if (Count is not null)
        {
            var usedCount = whereCount ?? allCount;

            resultBody = Count.Operator switch
            {
                ComparableNumberSearchOperator.Equal => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
                    ? anyCall
                    : Expression.Equal(usedCount, Expression.Constant(Count.Keys[0])),
                ComparableNumberSearchOperator.In =>
                    Expression.Call(
                        null,
                        _enumerableContains,
                        [Expression.Constant(Count.Keys, typeof(IEnumerable<int>)), usedCount]
                    ),
                ComparableNumberSearchOperator.LessThan => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
                    ? anyCall
                    : Expression.LessThan(usedCount, Expression.Constant(Count.Keys[0])),
                ComparableNumberSearchOperator.LessThanOrEqual => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
                    ? anyCall
                    : Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
                ComparableNumberSearchOperator.GreaterThan => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
                    ? anyCall
                    : Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
                ComparableNumberSearchOperator.GreaterThanOrEqual => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
                    ? anyCall
                    : Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
                ComparableNumberSearchOperator.BetweenOpen =>
                    Expression.AndAlso(
                        Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
                        Expression.LessThan(usedCount, Expression.Constant(Count.Keys[1]))
                    ),
                ComparableNumberSearchOperator.BetweenLeftClosed =>
                    Expression.AndAlso(
                        Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
                        Expression.LessThan(usedCount, Expression.Constant(Count.Keys[1]))
                    ),
                ComparableNumberSearchOperator.BetweenRightClosed =>
                    Expression.AndAlso(
                        Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
                        Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[1]))
                    ),
                ComparableNumberSearchOperator.BetweenClosed =>
                    Expression.AndAlso(
                        Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
                        Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[1]))
                    ),
                _ => throw new InvalidDataException(nameof(Count.Operator)),
            };
            if (Count.Reverse) resultBody = Expression.Not(resultBody);
        }
        else if (Percent is not null)
        {
            Debug.Assert(whereCount is not null);

            Expression doubleAllCount = Expression.Convert(allCount, typeof(double));
            whereCount = Expression.Convert(whereCount, typeof(double));
            Expression usedPercent = Expression.Divide(whereCount, doubleAllCount);

            var queryableAny = _queryableAnyOfT.MakeGenericMethod(typeof(T));
            usedPercent = Expression.Condition(Expression.Not(Expression.Call(null, queryableAny, originalQueryable)), Expression.Constant(0.0), usedPercent);

            resultBody = Percent.Operator switch
            {
                ComparableNumberSearchOperator.Equal => Expression.Equal(usedPercent, Expression.Constant(Percent.Keys[0])),
                ComparableNumberSearchOperator.In =>
                    Expression.Call(
                        null,
                        _enumerableContains,
                        [Expression.Constant(Percent.Keys, typeof(IEnumerable<double>)), usedPercent]
                    ),
                ComparableNumberSearchOperator.LessThan => Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[0])),
                ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
                ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
                ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
                ComparableNumberSearchOperator.BetweenOpen =>
                    Expression.AndAlso(
                        Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
                        Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[1]))
                    ),
                ComparableNumberSearchOperator.BetweenLeftClosed =>
                    Expression.AndAlso(
                        Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
                        Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[1]))
                    ),
                ComparableNumberSearchOperator.BetweenRightClosed =>
                    Expression.AndAlso(
                        Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
                        Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[1]))
                    ),
                ComparableNumberSearchOperator.BetweenClosed =>
                    Expression.AndAlso(
                        Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
                        Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[1]))
                    ),
                _ => throw new InvalidDataException(nameof(Percent.Operator)),
            };
            if (Percent.Reverse) resultBody = Expression.Not(resultBody);
        }

        Debug.Assert(resultBody is not null);

        var result = Expression.Lambda<Func<TOwner, bool>>(resultBody, memberAccessor.Parameters);

        return this.ApplyReversiblePredicate(result);

        static bool TryUseAnyCall(
            ComparableNumberSearchOperator @operator,
            int? key,
            Expression toCallAny,
            [NotNullWhen(true)] out Expression? result)
        {
            ArgumentNullException.ThrowIfNull(toCallAny);

            (bool shouldUseAny, bool shouldReverseAny) = (@operator, key) switch
            {
                (ComparableNumberSearchOperator.Equal, 0) => (true, true),
                (ComparableNumberSearchOperator.LessThan, 1) => (true, true),
                (ComparableNumberSearchOperator.LessThanOrEqual, 0) => (true, true),
                (ComparableNumberSearchOperator.GreaterThan, 0) => (true, false),
                (ComparableNumberSearchOperator.GreaterThanOrEqual, 1) => (true, false),
                _ => (false, false),
            };

            result = null;
            if (shouldUseAny)
            {
                result = Expression.Call(
                    null,
                    _queryableAnyOfT.MakeGenericMethod(typeof(T)),
                    [toCallAny]
                );

                if (shouldReverseAny)
                {
                    result = Expression.Not(result);
                }

                return true;
            }

            return false;
        }
    }
}

对于集合类型的属性,笔者实现了计数和比例比较,用于筛选符合条件的元素数量或占比是否符合条件。如果需要,各种聚合条件也应该可以实现,此处不再列举。TryUseAnyCall()方法对计数条件尝试使用Any()替换Count(),例如Count > 0等价于Any(),这可以在EF Core中生成更高效的SQL(据说EF Core准备在内部添加这个优化)。

辅助类型

csharp 复制代码
internal static class ScalarSearchFilterExtensions
{
    private static readonly MethodInfo _enumerableContainsOfT = typeof(Enumerable)
        .GetMethods()
        .Where(static m => m.Name is nameof(Enumerable.Contains))
        .Single(static m => m.GetParameters().Length is 2);

    internal static string? GetKeysCheckMessage<T>(ICollection<T> keys, ComparableNumberSearchOperator @operator)
    {
        string? exceptionHint = null;
        switch (@operator)
        {
            case ComparableNumberSearchOperator.Equal:
                if (keys is { Count: > 1 })
                {
                    exceptionHint = $"必须设置一个元素。";
                }
                break;
            case ComparableNumberSearchOperator.In:
                if (keys is { Count: < 2 })
                {
                    exceptionHint = $"必须设置多个元素。";
                }
                break;
            case ComparableNumberSearchOperator.LessThan:
                goto case ComparableNumberSearchOperator.Equal;
            case ComparableNumberSearchOperator.LessThanOrEqual:
                goto case ComparableNumberSearchOperator.Equal;
            case ComparableNumberSearchOperator.GreaterThan:
                goto case ComparableNumberSearchOperator.Equal;
            case ComparableNumberSearchOperator.GreaterThanOrEqual:
                goto case ComparableNumberSearchOperator.Equal;
            case ComparableNumberSearchOperator.BetweenOpen:
                goto case ComparableNumberSearchOperator.BetweenClosed;
            case ComparableNumberSearchOperator.BetweenLeftClosed:
                goto case ComparableNumberSearchOperator.BetweenClosed;
            case ComparableNumberSearchOperator.BetweenRightClosed:
                goto case ComparableNumberSearchOperator.BetweenClosed;
            case ComparableNumberSearchOperator.BetweenClosed:
                if (keys is { Count: not 2 })
                {
                    exceptionHint = $"必须设置两个元素。";
                }
                break;
            case ComparableNumberSearchOperator.InBetweenOpen:
                if (keys is { Count: < 4 } || keys.Count % 2 != 0)
                {
                    exceptionHint = $"必须设置不少于四个的偶数个元素。";
                }
                break;
            case ComparableNumberSearchOperator.InBetweenLeftClosed:
                goto case ComparableNumberSearchOperator.InBetweenOpen;
            case ComparableNumberSearchOperator.InBetweenRightClosed:
                goto case ComparableNumberSearchOperator.InBetweenOpen;
            case ComparableNumberSearchOperator.InBetweenClosed:
                goto case ComparableNumberSearchOperator.InBetweenOpen;
            default:
                exceptionHint = "的元素数量错误。";
                break;
        };

        if (exceptionHint is not null) return $"当 {nameof(@operator)} 的值为 {@operator} 时{exceptionHint}";
        else return null;
    }

    internal static void NullKeyCheck<T>(IReadOnlyList<T?> keys)
        where T : struct
    {
        if (keys.Any(static n => n is null)) throw new InvalidOperationException("不能使用值为空的元素搜索值不能为空的成员。");
    }

    internal static Expression<Func<TOwner, bool>> GetWherePredicateExtension<TOwner, TNumber>(
        IReadOnlyList<TNumber?> keys,
        ComparableNumberSearchOperator @operator,
        Expression<Func<TOwner, TNumber>> memberAccessor)
        where TNumber : struct
    {
        var typeOfNumber = typeof(TNumber);
        var _enumerableContains = @operator is ComparableNumberSearchOperator.In ? _enumerableContainsOfT.MakeGenericMethod([typeOfNumber]) : null;

        Expression newBody = @operator switch
        {
            ComparableNumberSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
            ComparableNumberSearchOperator.In => Expression.Call(null, _enumerableContains!, [Expression.Constant(keys.Cast<TNumber>().ToList(), typeof(IEnumerable<TNumber>)), memberAccessor.Body]),
            ComparableNumberSearchOperator.LessThan => Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
            ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
            ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
            ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
            ComparableNumberSearchOperator.BetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
            ComparableNumberSearchOperator.BetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
            ComparableNumberSearchOperator.BetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
            ComparableNumberSearchOperator.BetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
            ComparableNumberSearchOperator.InBetweenOpen => CombineInBetweenBody(keys, @operator, memberAccessor),
            ComparableNumberSearchOperator.InBetweenLeftClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
            ComparableNumberSearchOperator.InBetweenRightClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
            ComparableNumberSearchOperator.InBetweenClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
            _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
        };

        return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
    }

    internal static Expression<Func<TOwner, bool>> GetWherePredicateExtension<TOwner, TNumber>(
        IReadOnlyList<TNumber?> keys,
        ComparableNumberSearchOperator @operator,
        Expression<Func<TOwner, TNumber?>> memberAccessor)
        where TNumber : struct
    {
        var typeOfNullableNumber = typeof(TNumber?);
        var enumerableContains = @operator is ComparableNumberSearchOperator.In ? _enumerableContainsOfT.MakeGenericMethod([typeOfNullableNumber]) : null;

        Expression newBody = @operator switch
        {
            ComparableNumberSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
            ComparableNumberSearchOperator.In => Expression.Call(null, enumerableContains!, [Expression.Constant(keys, typeof(IEnumerable<TNumber?>)), memberAccessor.Body]),
            ComparableNumberSearchOperator.LessThan => Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
            ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
            ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
            ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
            ComparableNumberSearchOperator.BetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.BetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.BetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.BetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.InBetweenOpen => CombineInBetweenBody(keys, @operator, memberAccessor),
            ComparableNumberSearchOperator.InBetweenLeftClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
            ComparableNumberSearchOperator.InBetweenRightClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
            ComparableNumberSearchOperator.InBetweenClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
            _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
        };

        return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
    }

    private static Expression CombineInBetweenBody<TOwner, TNumber>(
        IReadOnlyList<TNumber?> keys,
        ComparableNumberSearchOperator @operator,
        Expression<Func<TOwner, TNumber>> memberAccessor)
        where TNumber : struct
    {
        var typeOfNumber = typeof(TNumber);
        var keysGroups = keys.Chunk(2);

        Expression newBody = @operator switch
        {
            ComparableNumberSearchOperator.InBetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
            ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
            ComparableNumberSearchOperator.InBetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
            ComparableNumberSearchOperator.InBetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
            _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
        };

        foreach (var inKeys in keysGroups.Skip(1))
        {
            newBody = @operator switch
            {
                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)))),
                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)))),
                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)))),
                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)))),
                _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
            };
        }

        return newBody;
    }

    private static Expression CombineInBetweenBody<TOwner, TNumber>(
        IReadOnlyList<TNumber?> keys,
        ComparableNumberSearchOperator @operator,
        Expression<Func<TOwner, TNumber?>> memberAccessor)
        where TNumber : struct
    {
        var typeOfNullableNumber = typeof(TNumber?);

        var keysGroups = keys.Chunk(2);

        Expression newBody = @operator switch
        {
            ComparableNumberSearchOperator.InBetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.InBetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
            ComparableNumberSearchOperator.InBetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
            _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
        };

        foreach (var inKeys in keysGroups.Skip(1))
        {
            newBody = @operator switch
            {
                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)))),
                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)))),
                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)))),
                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)))),
                _ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
            };
        }

        return newBody;
    }
}

辅助类型用于统一定义表达式的拼接方式等,因为这些拼接对于大多数基础数据类型来说都是通用的。

扩展复杂筛选支持

有时查询条件可能比较复杂,单个生成器无法表达。因此需要一个用于描述更复杂的条件的生成器,单个生成器无法描述的情况使用多个生成器组合来实现。既然复杂条件是由单个条件组合而来,最好能复用已经定义好的单个筛选器。

csharp 复制代码
/// <summary>
/// 支持复杂条件嵌套的高级查询条件构造器接口
/// </summary>
/// <typeparam name="TFilterPredicateBuilder">基础查询条件构造器</typeparam>
/// <typeparam name="T">要查询的数据类型</typeparam>
public interface IAdvancedFilterPredicateBuilder<out TFilterPredicateBuilder, T> : IFilterPredicateBuilder<T>
    where TFilterPredicateBuilder : IFilterPredicateBuilder<T>
{
    /// <summary>
    /// 基础查询条件集合
    /// </summary>
    IReadOnlyList<TFilterPredicateBuilder>? Filters { get; }

    /// <summary>
    /// 高级查询条件组集合
    /// </summary>
    IReadOnlyList<IAdvancedFilterPredicateBuilder<TFilterPredicateBuilder, T>>? FilterGroups { get; }
}

高级查询接口允许组合和嵌套任意多个筛选器以实现更复杂的条件。

csharp 复制代码
/// <summary>
/// 支持复杂条件嵌套的高级查询构造器
/// </summary>
/// <typeparam name="TQueryBuilder">基础查询构造器</typeparam>
/// <typeparam name="T">要查询的数据类型</typeparam>
/// <param name="Queries">基础查询条件集合</param>
/// <param name="QueryGroups">高级查询条件组集合</param>
/// <param name="CombineType">条件组合方式</param>
/// <param name="Reverse">是否反转条件</param>
public record AdvancedQueryBuilder<TQueryBuilder, T>(
    ImmutableList<TQueryBuilder>? Queries = null,
    ImmutableList<AdvancedQueryBuilder<TQueryBuilder, T>>? QueryGroups = null,
    [EnumDataType(typeof(PredicateCombineKind))]
    PredicateCombineKind? CombineType = PredicateCombineKind.And,
    bool Reverse = false)
    : IAdvancedFilterPredicateBuilder<TQueryBuilder, T>
    , IPredicateReversible
    where TQueryBuilder : IFilterPredicateBuilder<T>
{
    /// <inheritdoc/>
    public IReadOnlyList<TQueryBuilder>? Filters => Queries;

    /// <inheritdoc/>
    public IReadOnlyList<IAdvancedFilterPredicateBuilder<TQueryBuilder, T>>? FilterGroups => QueryGroups;

    /// <summary>
    /// 获取查询条件
    /// </summary>
    /// <returns>组合完成的查询条件</returns>
    public Expression<Func<T, bool>>? GetWherePredicate()
    {
        var where = CombinePredicates(Queries?.Select(static q => q.GetWherePredicate())
            .Concat(QueryGroups?.Select(static qg => qg.GetWherePredicate()) ?? [])
            .Where(static p => p is not null)!);

        return this.ApplyReversiblePredicate(where);
    }

    /// <summary>
    /// 组合查询条件
    /// </summary>
    /// <param name="predicates">待组合的子条件</param>
    /// <returns></returns>
    /// <exception cref="NotSupportedException"></exception>
    protected Expression<Func<T, bool>>? CombinePredicates(IEnumerable<Expression<Func<T, bool>>>? predicates)
    {
        var predicate = predicates?.FirstOrDefault();
        if (predicates?.Any() is true)
        {
            predicate = CombineType switch
            {
                PredicateCombineKind.And => predicates?.AndAlsoAll(),
                PredicateCombineKind.Or => predicates?.OrElseAll(),
                _ => throw new NotSupportedException(CombineType.ToString()),
            };
        }

        return predicate;
    }
}

单个条件可以获取对应的筛选表达式,那么这些条件的组合也就是对这些表达式进行组合。

添加排序和分页支持

分页的前提是排序,否则分页的结果不稳定。对于分页,通常还想获得符合条件的数据总量用于计算页数,但是排序并不影响总数计算。如果能在计算总数时忽略排序,仅在获取某一页数据时使用排序最好。这就要求我们分别存储和处理筛选条件和排序条件。筛选条件的问题已经在前面解决了,这里只需要关注排序条件的问题。

排序的概念接口

csharp 复制代码
/// <summary>
/// 排序查询构造器接口
/// </summary>
/// <typeparam name="T">查询的元素类型</typeparam>
public interface IOrderedQueryBuilder<T>
{
    /// <summary>
    /// 对查询应用排序
    /// </summary>
    /// <param name="query">原始查询</param>
    /// <returns>已排序的查询</returns>
    IOrderedQueryable<T> ApplyOrder(IQueryable<T> query);
}

/// <summary>
/// 可排序查询接口
/// </summary>
/// <typeparam name="T">查询的元素类型</typeparam>
/// <typeparam name="TOrderKey">可用的排序关键字枚举类型</typeparam>

public interface IKeySelectorOrderedQueryBuilder<T, TOrderKey> : IOrderedQueryBuilder<T>
    where TOrderKey : struct, Enum
{
    /// <summary>
    /// 获取支持的排序关键字选择器
    /// </summary>
    IReadOnlyDictionary<TOrderKey, Expression<Func<T, object?>>> GetSupportedOrderKeySelectors();

    /// <summary>
    /// 排序关键字信息
    /// </summary>
    ImmutableList<OrderInfo<TOrderKey>>? OrderKeys { get; }
}

/// <summary>
/// 排序信息
/// </summary>
/// <typeparam name="T">排序对象的类型</typeparam>
/// <param name="Key">排序关键字</param>
/// <param name="OrderKind">排序方式</param>
public record OrderInfo<T>(
    T Key,
    [EnumDataType(typeof(OrderKind))]
    OrderKind OrderKind = OrderKind.Asc)
    where T : struct, Enum
{
    /// <summary>
    /// 排序关键字
    /// </summary>
    public T Key { get; } = CheckOrderKey(Key);

    private static T CheckOrderKey(T value)
    {
        if (!Enum.IsDefined(value)) throw new InvalidEnumArgumentException(nameof(Key), int.Parse(value.ToString()), typeof(T));
        return value;
    }
}

/// <summary>
/// 排序方式
/// </summary>
public enum OrderKind
{
    /// <summary>
    /// 升序
    /// </summary>
    Asc = 1,

    /// <summary>
    /// 降序
    /// </summary>
    Desc = 2,
}

同样的,第一个接口只描述如何为查询附加排序,第二个接口描述序列化传输的方式。LINQ中的排序方法参数是一个排序关键字属性访问表达式,属性的类型就是表达式的返回值类型,属性类型千变万化,因此只能使用返回object的表达式来存储。表达式关键字本身无法序列化传输,因此笔者选择使用枚举来指代表达式,这也同时限定了可用于排序的属性,有利于安全。

分页的概念接口

目前有两种流行的分页方式,偏移量分页和游标分页。偏移量分页支持随机页码跳转,也需要提前计算数据总数,数据量大或访问尾部页码时性能会下降。游标分页则是根据唯一键游标排序来进行,可以是两个游标之间的数据或单个游标和获取数据量的组合。在返回的结果中需要附带相邻页面的起始游标,因此实际查询数据时需要多查一条数据,多查的这一条数据只需要游标值,不需要数据本身。本文以偏移量分页为例。

csharp 复制代码
/// <summary>
/// 可页码分页查询接口
/// </summary>
public interface IOffsetPagingSupport
{
    /// <summary>
    /// 分页信息
    /// </summary>
    OffsetPageInfo OffsetPage { get; }
}

/// <summary>
/// 页码分页信息
/// </summary>
/// <param name="PageIndex">页码</param>
/// <param name="PageSize">页面大小</param>
public record OffsetPageInfo(
    [Range(1, int.MaxValue, ErrorMessage = DataAnnotationErrorMessageDefaults.Range)] int PageIndex = 1,
    [Range(1, int.MaxValue, ErrorMessage = DataAnnotationErrorMessageDefaults.Range)] int PageSize = 10
)
{
    /// <summary>
    /// 跳过的页数
    /// </summary>
    public int SkipedPageCount => PageIndex - 1;

    /// <summary>
    /// 跳过的元素数量
    /// </summary>
    public int SkipedElementCount => SkipedPageCount * PageSize;
}

/// <summary>
/// 页码分页查询构造扩展
/// </summary>
public static class OffsetPageQueryBuilderExtensions
{
    /// <summary>
    /// 页码分页
    /// </summary>
    /// <typeparam name="T">查询的元素类型</typeparam>
    /// <param name="offsetPaging">分页信息</param>
    /// <param name="query">要应用分页的查询</param>
    /// <returns>已页码分页的查询</returns>
    public static IQueryable<T> OffsetPage<T>(this IOffsetPagingSupport offsetPaging, IQueryable<T> query)
    {
        ArgumentNullException.ThrowIfNull(offsetPaging);
        ArgumentNullException.ThrowIfNull(query);
        var paging = offsetPaging.OffsetPage;

        return query.Skip(paging.SkipedElementCount).Take(paging.PageSize);
    }
}

完整的分页查询生成器

csharp 复制代码
/// <summary>
/// 分页查询构造器
/// </summary>
/// <typeparam name="TQueryBuilder">查询构造器类型</typeparam>
/// <typeparam name="T">查询的数据类型</typeparam>
/// <param name="Query">基础查询</param>
/// <param name="OffsetPage">分页信息</param>
public abstract record OffsetPagedQueryBuilder<TQueryBuilder, T>(
    TQueryBuilder Query,
    OffsetPageInfo? OffsetPage = null)
    : IFilterPredicateBuilder<T>
    , IOrderedQueryBuilder<T>
    , IOffsetPagingSupport
    where TQueryBuilder : IFilterPredicateBuilder<T>
{
    /// <inheritdoc/>
    public Expression<Func<T, bool>>? GetWherePredicate() => Query.GetWherePredicate();

    /// <inheritdoc/>
    public virtual OffsetPageInfo OffsetPage { get; } = OffsetPage ?? new();

    /// <inheritdoc/>
    public abstract IOrderedQueryable<T> ApplyOrder(IQueryable<T> query);
}

/// <summary>
/// 查询的排序方法
/// </summary>
public enum QueryableOrderMethod
{
    /// <summary>
    /// 优先升序
    /// </summary>
    OrderBy = 1,

    /// <summary>
    /// 优先降序
    /// </summary>
    OrderByDescending,

    /// <summary>
    /// 次一级升序
    /// </summary>
    ThenBy,

    /// <summary>
    /// 次一级降序
    /// </summary>
    ThenByDescending
}

/// <summary>
/// 关键字排序查询构造器扩展
/// </summary>
public static class KeySelectorOrderQueryBuilderExtensions
{
    private static readonly MethodInfo _queryableOederByOfT = typeof(Queryable).GetMethods()
        .Single(static m => m.Name is nameof(Queryable.OrderBy) && m.GetParameters().Length is 2);

    private static readonly MethodInfo _queryableThenByOfT = typeof(Queryable).GetMethods()
        .Single(static m => m.Name is nameof(Queryable.ThenBy) && m.GetParameters().Length is 2);

    private static readonly MethodInfo _queryableOrderByDescendingOfT = typeof(Queryable).GetMethods()
        .Single(static m => m.Name is nameof(Queryable.OrderByDescending) && m.GetParameters().Length is 2);

    private static readonly MethodInfo _queryableThenByDescendingOfT = typeof(Queryable).GetMethods()
        .Single(static m => m.Name is nameof(Queryable.ThenByDescending) && m.GetParameters().Length is 2);

    /// <summary>
    /// 对查询应用关键字排序
    /// </summary>
    /// <typeparam name="T">查询的元素类型</typeparam>
    /// <typeparam name="TOrderKey">可用的排序关键字类型</typeparam>
    /// <param name="OrderInfos">排序信息</param>
    /// <param name="query">原始查询</param>
    /// <returns>已排序的查询</returns>
    /// <exception cref="InvalidDataException"></exception>
    public static IOrderedQueryable<T> ApplyKeyedOrder<T, TOrderKey>(this IKeySelectorOrderedQueryBuilder<T, TOrderKey> OrderInfos, IQueryable<T> query)
        where TOrderKey : struct, Enum
    {
        ArgumentNullException.ThrowIfNull(OrderInfos);
        ArgumentNullException.ThrowIfNull(query);
        if (OrderInfos.GetSupportedOrderKeySelectors()?.Count > 0 is false) throw new InvalidDataException($"{nameof(OrderInfos.GetSupportedOrderKeySelectors)}");

        IOrderedQueryable<T> orderedQuery;
        QueryableOrderMethod methodKind;
        MethodInfo orderMethod;
        Expression<Func<T, object?>> keySelector;

        var firstOrder = OrderInfos.OrderKeys?.FirstOrDefault();
        if (firstOrder is not null)
        {
            methodKind = firstOrder.OrderKind switch
            {
                OrderKind.Asc => QueryableOrderMethod.OrderBy,
                OrderKind.Desc => QueryableOrderMethod.OrderByDescending,
                _ => throw new InvalidDataException($"{nameof(OrderKind)}"),
            };

            keySelector = OrderInfos.GetSupportedOrderKeySelectors()[firstOrder.Key];
            orderMethod = GetQueryOrderMethod<T>(methodKind, keySelector.ReturnType);
            orderedQuery = (IOrderedQueryable<T>)orderMethod.Invoke(null, [query, keySelector])!;
        }
        else
        {
            keySelector = OrderInfos.GetSupportedOrderKeySelectors().First().Value;
            orderedQuery = (IOrderedQueryable<T>)(GetQueryOrderMethod<T>(QueryableOrderMethod.OrderBy, keySelector.ReturnType)
                .Invoke(null, [query, keySelector]))!;
        }

        foreach (var subsequentOrder in OrderInfos.OrderKeys?.Skip(1) ?? [])
        {
            if (subsequentOrder is not null)
            {
                methodKind = subsequentOrder.OrderKind switch
                {
                    OrderKind.Asc => QueryableOrderMethod.ThenBy,
                    OrderKind.Desc => QueryableOrderMethod.ThenByDescending,
                    _ => throw new InvalidDataException($"{nameof(OrderKind)}"),
                };

                keySelector = OrderInfos.GetSupportedOrderKeySelectors()[subsequentOrder.Key];
                orderMethod = GetQueryOrderMethod<T>(methodKind, keySelector.ReturnType);
                orderedQuery = (IOrderedQueryable<T>)orderMethod.Invoke(null, [orderedQuery, keySelector])!;
            }
        }

        return orderedQuery;
    }

    private static MethodInfo GetQueryOrderMethod<T>(QueryableOrderMethod method, Type orderKeyType)
    {
        return method switch
        {
            QueryableOrderMethod.OrderBy => _queryableOederByOfT.MakeGenericMethod(typeof(T), orderKeyType),
            QueryableOrderMethod.OrderByDescending => _queryableOrderByDescendingOfT.MakeGenericMethod(typeof(T), orderKeyType),
            QueryableOrderMethod.ThenBy => _queryableThenByOfT.MakeGenericMethod(typeof(T), orderKeyType),
            QueryableOrderMethod.ThenByDescending => _queryableThenByDescendingOfT.MakeGenericMethod(typeof(T), orderKeyType),
            _ => throw new InvalidDataException($"{nameof(method)}"),
        };
    }
}

此处的抽象基类不实现关键字排序是因为无法确定最终查询是否支持关键字排序,有可能是在代码中定义的静态排序规则。如果确实支持关键字排序,在最终类型上实现关键字排序接口即可。扩展类型则用于快速实现关键字排序表达式生成。

使用示例

演示用数据类型

csharp 复制代码
/// <summary>
/// 示例1
/// </summary>
public class Entity1
{
    public int Id { get; set; }

    public string? Text1 { get; set; }

    public Entity2? Entity2 { get; set; }

    public List<Entity3> Entities3 { get; set; } = [];
}

/// <summary>
/// 示例2
/// </summary>
public class Entity2
{
    public int Id { get; set; }

    public string? Text2 { get; set; }
}

/// <summary>
/// 示例3
/// </summary>
public class Entity3
{
    public int Id { get; set; }

    public string? Text3 { get; set; }

    public Entity1? Entity1 { get; set; }
}

基础查询定义

csharp 复制代码
/// <summary>
/// Entity1查询生成器
/// </summary>
/// <param name="Text1">Entity1文本</param>
/// <param name="Id">Entity1的Id</param>
/// <param name="Entity2">Entity1的Entity2筛选</param>
/// <param name="Entities3">Entity1的Entity3集合筛选</param>
/// <param name="Reverse">是否反转条件</param>
/// <inheritdoc cref="QueryBuilderBase{T}"/>
/// <remarks>
/// 反转条件由<see cref="QueryBuilderBase{T}"/>在<see cref="QueryBuilderBase{T}.GetWherePredicate()"/>中自动进行,此处不需要处理。
/// </remarks>
public sealed record Entity1QueryBuilder(
    NumberSearchFilter<int>? Id = null,
    StringSearchFilter? Text1 = null,
    Entity2QueryBuilder? Entity2 = null,
    CollectionMemberSearchFilter<Entity3QueryBuilder, Entity3>? Entities3 = null,
    [EnumDataType(typeof(PredicateCombineKind))]
    PredicateCombineKind? CombineType = PredicateCombineKind.And,
    bool Reverse = false)
    : QueryBuilderBase<Entity1>(CombineType)
    , IPredicateReversible
{
    /// <inheritdoc/>
    protected override Expression<Func<Entity1, bool>>? BuildWherePredicate()
    {
        List<Expression<Func<Entity1, bool>>> predicates = [];

        predicates.AddIfNotNull(Id?.GetWherePredicate<Entity1>(e1 => e1.Id));
        predicates.AddIfNotNull(Text1?.GetWherePredicate<Entity1>(e1 => e1.Text1!));
        predicates.AddIfNotNull(Entity2?.GetWherePredicate<Entity1>(e1 => e1.Entity2!));
        predicates.AddIfNotNull(Entities3?.GetWherePredicate<Entity1>(e1 => e1.Entities3));
        predicates.AddIfNotNull(base.BuildWherePredicate());

        var where = CombinePredicates(predicates);

        return where;
    }
}

/// <summary>
/// Entity2查询生成器
/// </summary>
/// <param name="Text2">Entity2文本</param>
/// <param name="Id">Entity2的Id</param>
/// <param name="Reverse">是否反转条件</param>
/// <inheritdoc cref="QueryBuilderBase{T}"/>
public sealed record Entity2QueryBuilder(
    NumberSearchFilter<int>? Id = null,
    StringSearchFilter? Text2 = null,
    [EnumDataType(typeof(PredicateCombineKind))]
    PredicateCombineKind? CombineType = PredicateCombineKind.And,
    bool Reverse = false)
    : QueryBuilderBase<Entity2>(CombineType)
    , IPredicateReversible
{
    /// <inheritdoc/>
    protected override Expression<Func<Entity2, bool>>? BuildWherePredicate()
    {
        List<Expression<Func<Entity2, bool>>> predicates = [];

        predicates.AddIfNotNull(Id?.GetWherePredicate<Entity2>(e1 => e1.Id));
        predicates.AddIfNotNull(Text2?.GetWherePredicate<Entity2>(e2 => e2.Text2!));
        predicates.AddIfNotNull(base.BuildWherePredicate());

        var where = CombinePredicates(predicates);

        return where;
    }
}

/// <summary>
/// Entity3查询生成器
/// </summary>
/// <param name="Text3">Entity3文本</param>
/// <param name="Entity1">Entity3的Entity1筛选</param>
/// <param name="Id">Entity3的Id</param>
/// <param name="Reverse">是否反转条件</param>
/// <inheritdoc cref="QueryBuilderBase{T}"/>
public sealed record Entity3QueryBuilder(
    NumberSearchFilter<int>? Id = null,
    StringSearchFilter? Text3 = null,
    Entity1QueryBuilder? Entity1 = null,
    [EnumDataType(typeof(PredicateCombineKind))]
    PredicateCombineKind? CombineType = PredicateCombineKind.And,
    bool Reverse = false)
    : QueryBuilderBase<Entity3>(CombineType)
    , IPredicateReversible
{
    /// <inheritdoc/>
    protected override Expression<Func<Entity3, bool>>? BuildWherePredicate()
    {
        List<Expression<Func<Entity3, bool>>> predicates = [];

        predicates.AddIfNotNull(Id?.GetWherePredicate<Entity3>(e3 => e3.Id));
        predicates.AddIfNotNull(Text3?.GetWherePredicate<Entity3>(e3 => e3.Text3!));
        predicates.AddIfNotNull(Entity1?.GetWherePredicate<Entity3>(e3 => e3.Entity1!));
        predicates.AddIfNotNull(base.BuildWherePredicate());

        var where = CombinePredicates(predicates);

        return where;
    }
}

public static class CollectionExtensions
{
    /// <summary>
    /// 如果<paramref name="item"/>不是<see langword="null"/>,把<paramref name="item"/>添加到<paramref name="collection"/>。
    /// </summary>
    /// <typeparam name="T">集合的元素类型。</typeparam>
    /// <param name="collection">待添加元素的集合。</param>
    /// <param name="item">要添加的元素。</param>
    /// <returns>是否成功把元素添加到集合。</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool AddIfNotNull<T>(this ICollection<T> collection, T? item)
    {
        ArgumentNullException.ThrowIfNull(collection, nameof(collection));

        if (item is not null)
        {
            collection.Add(item);
            return true;
        }

        return false;
    }
}

从示例中可以看出,只需要针对数据类型的基础数据使用标量过滤器类型实现基础筛选,对于引用的其他数据类型,可以直接复用引用类型的查询生成器,并使用由组合查询生成接口提供的组合方法即可自动把复杂类型的筛选条件嵌套到当前类型的属性上。

分页查询

csharp 复制代码
/// <summary>
/// Entity1分页查询生成器
/// </summary>
/// <inheritdoc cref="OffsetPagedQueryBuilder{TQueryBuilder, T2}"/>
public sealed record OffsetPagedEntity1QueryBuilder(
    Entity1QueryBuilder Query,
    OffsetPageInfo? OffsetPage = null,
    ImmutableList<OrderInfo<Entity1OrderKey>>? OrderKeys = null)
    : OffsetPagedQueryBuilder<Entity1QueryBuilder, Entity1>(Query, OffsetPage)
    , IKeySelectorOrderedQueryBuilder<Entity1, Entity1OrderKey>
{
    /// <inheritdoc/>
    public IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> GetSupportedOrderKeySelectors() => Entity1OrderKeySelector.Content;

    /// <inheritdoc/>
    public override IOrderedQueryable<Entity1> ApplyOrder(IQueryable<Entity1> query) => this.ApplyKeyedOrder(query);
}

/// <summary>
/// Entity1排序关键字
/// </summary>
public enum Entity1OrderKey : uint
{
    /// <summary>
    /// Id
    /// </summary>
    Id = 1,

    /// <summary>
    /// Text1
    /// </summary>
    Text1
}

internal static class Entity1OrderKeySelector
{
    public static IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> Content { get; } =
        FrozenDictionary.ToFrozenDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>>([
            new(Entity1OrderKey.Id, e1 => e1.Id),
            new(Entity1OrderKey.Text1, e1 => e1.Text1),
        ]);
}

分页查询只要利用之前定义好的泛型基类填充类型参数即可。由于实例类型需要实现关键字排序,因此要实现相关接口。

高级分页查询

csharp 复制代码
/// <summary>
/// Entity1分页高级查询生成器
/// </summary>
/// <param name="OrderKeys">排序信息</param>
/// <inheritdoc cref="OffsetPagedQueryBuilder{TQueryBuilder, T}"/>
public sealed record OffsetPagedAdvancedEntity1QueryBuilder(
    AdvancedQueryBuilder<Entity1QueryBuilder, Entity1> Query,
    ImmutableList<OrderInfo<Entity1OrderKey>>? OrderKeys = null,
    OffsetPageInfo? OffsetPage = null)
    : OffsetPagedQueryBuilder<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>, Entity1>(
        Query,
        OffsetPage)
    , IKeySelectorOrderedQueryBuilder<Entity1, Entity1OrderKey>
{
    /// <inheritdoc/>
    public IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> GetSupportedOrderKeySelectors() => Entity1OrderKeySelector.Content;

    /// <inheritdoc/>
    public override IOrderedQueryable<Entity1> ApplyOrder(IQueryable<Entity1> query) => this.ApplyKeyedOrder(query);
}

高级分页查询同样只需要填充预定义泛型基类的类型参数即可。

实例化查询生成器对象

csharp 复制代码
OffsetPagedAdvancedEntity1QueryBuilder queryBuilder =
    new OffsetPagedAdvancedEntity1QueryBuilder(
        Query: new AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>(
            Queries: [ // ImmutableList<Entity1QueryBuilder>
                new Entity1QueryBuilder (
                    Id: new NumberSearchFilter<int>([2], ComparableNumberSearchOperator.GreaterThan),
                    Text1: new StringSearchFilter(["aa"], StringSearchOperator.Contains),
                    Entity2: new Entity2QueryBuilder(
                        new NumberSearchFilter<int>([100]),
                        new StringSearchFilter(["ccc"])
                    ),
                    Entities3: null,
                    CombineType: PredicateCombineKind.Or,
                    Reverse: false
                ),
                new Entity1QueryBuilder(
                    Id: new NumberSearchFilter<int>([5], ComparableNumberSearchOperator.LessThan)
                )
            ],
            QueryGroups: [ // ImmutableList<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>>
                new AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>(
                    Queries: [ // ImmutableList<Entity1QueryBuilder>
                        new Entity1QueryBuilder(
                            Id: new NumberSearchFilter<int>([20], ComparableNumberSearchOperator.Equal),
                            Text1: new StringSearchFilter(["bb"], StringSearchOperator.Contains),
                            Entity2: null,
                            Entities3: new CollectionMemberSearchFilter<Entity3QueryBuilder, Entity3>(
                                query: new Entity3QueryBuilder(
                                    Id: null,
                                    Text3: new StringSearchFilter(["fff"], StringSearchOperator.StartsWith)
                                ),
                                count: new NumberSearchFilter < int >([50]),
                                percent: null,
                                reverse: false
                            ),
                            CombineType: PredicateCombineKind.And,
                            Reverse: false
                        )
                    ],
                    QueryGroups:[ // ImmutableList<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>>
                    ],
                    CombineType: PredicateCombineKind.Or,
                    Reverse: true
                )
            ],
            CombineType: PredicateCombineKind.And,
            Reverse: true
        ),
        OrderKeys: [ // ImmutableList<OrderInfo<Entity1OrderKey>>
            new OrderInfo<Entity1OrderKey>(Entity1OrderKey.Text1, OrderKind.Desc),
            new OrderInfo<Entity1OrderKey>(Entity1OrderKey.Id),
        ],
        OffsetPage: new OffsetPageInfo(1,20)
    );

在集合中使用生成器

csharp 复制代码
// 准备一个集合
var entity1Arr = new Entity1[
        new()
    ];
// 从生成器中获取筛选表达式
var where = queryBuilder.GetWherePredicate();
// 把集合转换为 IQueryable<Entity1> 使用表达式类型的参数
var query = entity1Arr.AsQueryable().Where(where!);
// 把排序应用到查询
var ordered = builder.ApplyOrder(query);
// 把分页应用到查询
var paged = builder.OffsetPage(ordered);
// 把表达式编译为委托
var whereFunc = where.Compile();

最终得到的筛选表达式

csharp 复制代码
Expression<Func<Entity1, bool>> exp = e1 =>
!(
    (
        e1.Id > 2
        || e1.Text1.Contains("aa")
        || (e1.Entity2.Id == 100 && e1.Entity2.Text2.Contains("ccc"))
    )
    && e1.Id < 5
    && !(
        e1.Id == 20
        && e1.Text1.Contains("bb")
        && e1.Entities3.AsQueryable().Count() == 50
    )
);

这个表达式是经过手动去除多余的括号,重新整理缩进后得到的版本。原始表达式像这样:

在此也推荐这个好用的VS插件:ReadableExpressions.Visualizers。这个插件可以把表达式显示成代码编辑器里的样子,对各种语法要素也会着色,调试动态拼接的表达式时非常好用。

EF Core生成的SQL(SQL Server)

sql 复制代码
DECLARE @__p_0 int = 0;
DECLARE @__p_1 int = 20;

SELECT [e].[Id], [e].[Entity2Id], [e].[Text1]
FROM [Entity1] AS [e]
LEFT JOIN [Entity2] AS [e0] ON [e].[Entity2Id] = [e0].[Id]
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 (
    SELECT COUNT(*)
    FROM [Entity3] AS [e1]
    WHERE [e].[Id] = [e1].[Entity1Id]) = 50)
ORDER BY [e].[Text1] DESC, [e].[Id]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

等价的JSON表示

json 复制代码
{
  "Query": {
    "Queries": [
      {
        "Id": {
          "Keys": [
            2
          ],
          "Operator": 8,
          "Reverse": false
        },
        "Text1": {
          "Keys": [
            "aa"
          ],
          "Operator": 4,
          "Reverse": false
        },
        "Entity2": {
          "Id": {
            "Keys": [
              100
            ],
            "Operator": 1,
            "Reverse": false
          },
          "Text2": {
            "Keys": [
              "ccc"
            ],
            "Operator": 4,
            "Reverse": false
          },
          "Reverse": false,
          "CombineType": 1
        },
        "Entities3": null,
        "Reverse": false,
        "CombineType": 2
      },
      {
        "Id": {
          "Keys": [
            5
          ],
          "Operator": 4,
          "Reverse": false
        },
        "Text1": null,
        "Entity2": null,
        "Entities3": null,
        "Reverse": false,
        "CombineType": 1
      }
    ],
    "QueryGroups": [
      {
        "Queries": [
          {
            "Id": {
              "Keys": [
                20
              ],
              "Operator": 1,
              "Reverse": false
            },
            "Text1": {
              "Keys": [
                "bb"
              ],
              "Operator": 4,
              "Reverse": false
            },
            "Entity2": null,
            "Entities3": {
              "Query": null,
              "Count": {
                "Keys": [
                  50
                ],
                "Operator": 1,
                "Reverse": false
              },
              "Percent": null,
              "Reverse": false
            },
            "Reverse": false,
            "CombineType": 1
          }
        ],
        "QueryGroups": [],
        "CombineType": 2,
        "Reverse": true,
        "Filters": [
          {
            "Id": {
              "Keys": [
                20
              ],
              "Operator": 1,
              "Reverse": false
            },
            "Text1": {
              "Keys": [
                "bb"
              ],
              "Operator": 4,
              "Reverse": false
            },
            "Entity2": null,
            "Entities3": {
              "Query": null,
              "Count": {
                "Keys": [
                  50
                ],
                "Operator": 1,
                "Reverse": false
              },
              "Percent": null,
              "Reverse": false
            },
            "Reverse": false,
            "CombineType": 1
          }
        ],
        "FilterGroups": []
      }
    ],
    "CombineType": 1,
    "Reverse": true,
    "Filters": [
      {
        "Id": {
          "Keys": [
            2
          ],
          "Operator": 8,
          "Reverse": false
        },
        "Text1": {
          "Keys": [
            "aa"
          ],
          "Operator": 4,
          "Reverse": false
        },
        "Entity2": {
          "Id": {
            "Keys": [
              100
            ],
            "Operator": 1,
            "Reverse": false
          },
          "Text2": {
            "Keys": [
              "ccc"
            ],
            "Operator": 4,
            "Reverse": false
          },
          "Reverse": false,
          "CombineType": 1
        },
        "Entities3": null,
        "Reverse": false,
        "CombineType": 2
      },
      {
        "Id": {
          "Keys": [
            5
          ],
          "Operator": 4,
          "Reverse": false
        },
        "Text1": null,
        "Entity2": null,
        "Entities3": null,
        "Reverse": false,
        "CombineType": 1
      }
    ],
    "FilterGroups": [
      {
        "Filters": [
          {
            "Id": {
              "Keys": [
                20
              ],
              "Operator": 1,
              "Reverse": false
            },
            "Text1": {
              "Keys": [
                "bb"
              ],
              "Operator": 4,
              "Reverse": false
            },
            "Entity2": null,
            "Entities3": {
              "Query": null,
              "Count": {
                "Keys": [
                  50
                ],
                "Operator": 1,
                "Reverse": false
              },
              "Percent": null,
              "Reverse": false
            },
            "Reverse": false,
            "CombineType": 1
          }
        ],
        "FilterGroups": []
      }
    ]
  },
  "OrderKeys": [
    {
      "OrderKind": 2,
      "Key": 2
    },
    {
      "OrderKind": 1,
      "Key": 1
    }
  ],
  "OffsetPage": {
    "PageIndex": 1,
    "PageSize": 20
  }
}

JSON中的属性如果是类型定义时的默认值,可以省略不写。例如字符串搜索的默认操作是包含子串,条件反转的默认值是false等。

特点总结

这套查询生成器是一个完全可组合的结构。从一组内置基础类型的筛选器开始组合出基层自定义类型的生成器,再通过基础筛选器和自定义生成器的组合继续组合出具有嵌套结构的类型的筛选器,最后通过泛型的分页和高级查询生成器组合出完整的查询生成器。这些查询生成器也可以独立使用,自由度很高。例如分页查询的总数计算,就可以只提取其中的筛选表达式部分来用,其中的各种自定义筛选器也都可以当作顶层筛选器来用。

基础类型的筛选器只实现组合生成器接口,因为基础类型一定是作为其他类型的属性来用的,所以针对基础类型的条件也一定要嫁接到一个属性访问表达式上才有意义。对于自定义表达式生成器,当作为顶级类型来使用时,表现为直接生成器,以当前类型为目标生成表达式;当作为其他类型的属性时,又表现为组合生成器,把生成的条件嫁接到上层对象的属性上。

筛选器的各个属性名和作用目标属性名完全无关,这样既隔离了内部代码和外部查询,使两边互不干扰,也能轻松对外部查询隐藏内部名称,降低安全风险。由于每个可查询的属性都是明确定义的,因此完全不存在恶意攻击的可能性,如果想对参数的范围之类的信息进行审查,结构化的查询数据也非常容易操作。从查询生成的定义中可以看出,查询的每一个片段都是静态表达式,因此生成器的所有部分都完全兼容静态编译检查和自动重构。

这个查询生成器解决了System.Linq.Dynamic.CoreLinqKit的劣势,相比较可能唯一的不便之处是代码量稍大,等价的JSON表示内容量较大,但是就因此获得的组合灵活性、序列化传输兼容性和静态安全性而言,这点代价还是可以接受的。

为了减少复杂查询的需要,笔者把查询关键字设计为数组类型,再根据操作检查具体数据。例如候选项查询,如果不直接支持,就只能使用高级查询生成器的基础生成器数组之间的Or连接来模拟。既然候选项查询必须使用数组型关键字,干脆充分利用这个数组的特点,直接提供区间查询,多个不连续区间查询等功能,最大程度减少对高级查询生成器的依赖,尽可能在简单查询生成器里实现绝大部部分常见条件。

如果直接把表达式编译成委托来用的话,可能会出现空引用异常,因为表达式不支持空传播运算符,只能直接访问。用EF Core生成SQL不会出现问题。

结语

很久以前笔者就思考过,利用LINQ实现动态表达式生成应该怎么办。刚开始发现JqGrid这个表格组件支持嵌套的复杂条件,并以嵌套的JSON结构来表示。后来又惊叹于了HotChocolate的自动条件参数生成和架构修改配置。开始思考动态生成的问题后又先后研究了System.Linq.Dynamic.CoreLinqKit等方案,分析总结了他们的特点和优劣。几经周折终于实现了这个比较满意表达式生成器。

像普通表达式生成器接口和组合表达式生成器接口就是研究过程中发现应该是两个不同的功能和接口才分离出来的。对于基础类型生成器,一定要嫁接到到其他类型的属性上才有用。而对于分页生成器来说又没有可组合的必要,要分页就说明应该是以顶级类型的身份来用。对于自定义类型的生成器来说又是两种都有可能。这样随着研究的深入问题逐步清晰的情况经常出现,而且构思阶段很难发现。

最开始分页生成器是没有通用泛型类的,需要自己继承,但是用了一段时间发现这个东西形态固定,实际上可以用泛型类实现。自动化条件反转和防止重复反转也是后来才发现和解决。

这次研究能顺利进行下去的一个关键是想到了对于复杂嵌套类型,可以把完整的条件表达式拆分为从顶级类型到目标类型的访问表达式和针对目标类型的条件表达式作为两个独立的部分来处理,然后使用表达式访问器拼合两个部分。这样使得生成器和数据类型一样可以自由组合。嵌套的表达式生成问题曾一直困扰着笔者,直到弄懂了表达式访问器的用法和打通了思路。

经过这次研究,对表达式的使用也更加熟练,收获颇丰。欢迎园友体验交流。

QQ群

读者交流QQ群:540719365

欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知笔者。

本文地址:如何在 .NET 中构建一个好用的动态查询生成器

相关推荐
Nemo_XP36 分钟前
Datatable和实体集合互转
c#·datatable
铭....39 分钟前
word批量导出visio图
开发语言·c#·word
Hare_bai1 小时前
WPF的基础控件:布局控件(StackPanel & DockPanel)
ui·c#·wpf·交互·xaml·visual studio
xdpcxq10292 小时前
.NET 开源工业视觉系统 OpenIVS 快速搭建自动化检测平台
开源·自动化·.net
yuanpan3 小时前
同为.net/C#的跨平台运行时的mono和.net Core有什么区别?
c#·.net·.netcore
Hare_bai5 小时前
WPF的交互核心:命令系统(ICommand)
ui·c#·wpf·交互·xaml
Eiceblue6 小时前
C# 将HTML文档、HTML字符串转换为图片
visualstudio·c#·xhtml
stormsha20 小时前
深入解析Kafka JVM堆内存:优化策略与监控实践
jvm·缓存·kafka·linq
进阶的小木桩20 小时前
C# 导出word 插入公式问题
开发语言·c#·word
天天代码码天天1 天前
PP-OCRv5 C++封装DLL C#调用源码分享
开发语言·c++·c#·ocr